processing_handler.go 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. package main
  2. import (
  3. "context"
  4. "fmt"
  5. "net/http"
  6. "strconv"
  7. "strings"
  8. "time"
  9. )
  10. var (
  11. processingSem chan struct{}
  12. headerVaryValue string
  13. fallbackImage *imageData
  14. )
  15. func initProcessingHandler() error {
  16. var err error
  17. processingSem = make(chan struct{}, conf.Concurrency)
  18. vary := make([]string, 0)
  19. if conf.EnableWebpDetection || conf.EnforceWebp {
  20. vary = append(vary, "Accept")
  21. }
  22. if conf.EnableClientHints {
  23. vary = append(vary, "DPR", "Viewport-Width", "Width")
  24. }
  25. headerVaryValue = strings.Join(vary, ", ")
  26. if fallbackImage, err = getFallbackImageData(); err != nil {
  27. return err
  28. }
  29. return nil
  30. }
  31. func respondWithImage(ctx context.Context, reqID string, r *http.Request, rw http.ResponseWriter, data []byte) {
  32. po := getProcessingOptions(ctx)
  33. var contentDisposition string
  34. if len(po.Filename) > 0 {
  35. contentDisposition = po.Format.ContentDisposition(po.Filename)
  36. } else {
  37. contentDisposition = po.Format.ContentDispositionFromURL(getImageURL(ctx))
  38. }
  39. rw.Header().Set("Content-Type", po.Format.Mime())
  40. rw.Header().Set("Content-Disposition", contentDisposition)
  41. if conf.SetCanonicalHeader {
  42. origin := getImageURL(ctx)
  43. if strings.HasPrefix(origin, "https://") || strings.HasPrefix(origin, "http://") {
  44. linkHeader := fmt.Sprintf(`<%s>; rel="canonical"`, origin)
  45. rw.Header().Set("Link", linkHeader)
  46. }
  47. }
  48. var cacheControl, expires string
  49. if conf.CacheControlPassthrough {
  50. cacheControl = getCacheControlHeader(ctx)
  51. expires = getExpiresHeader(ctx)
  52. }
  53. if len(cacheControl) == 0 && len(expires) == 0 {
  54. cacheControl = fmt.Sprintf("max-age=%d, public", conf.TTL)
  55. expires = time.Now().Add(time.Second * time.Duration(conf.TTL)).Format(http.TimeFormat)
  56. }
  57. if len(cacheControl) > 0 {
  58. rw.Header().Set("Cache-Control", cacheControl)
  59. }
  60. if len(expires) > 0 {
  61. rw.Header().Set("Expires", expires)
  62. }
  63. if len(headerVaryValue) > 0 {
  64. rw.Header().Set("Vary", headerVaryValue)
  65. }
  66. if conf.EnableDebugHeaders {
  67. imgdata := getImageData(ctx)
  68. rw.Header().Set("X-Origin-Content-Length", strconv.Itoa(len(imgdata.Data)))
  69. }
  70. rw.Header().Set("Content-Length", strconv.Itoa(len(data)))
  71. rw.WriteHeader(200)
  72. rw.Write(data)
  73. imageURL := getImageURL(ctx)
  74. logResponse(reqID, r, 200, nil, &imageURL, po)
  75. }
  76. func respondWithNotModified(ctx context.Context, reqID string, r *http.Request, rw http.ResponseWriter) {
  77. rw.WriteHeader(304)
  78. imageURL := getImageURL(ctx)
  79. logResponse(reqID, r, 304, nil, &imageURL, getProcessingOptions(ctx))
  80. }
  81. func handleProcessing(reqID string, rw http.ResponseWriter, r *http.Request) {
  82. ctx := r.Context()
  83. if newRelicEnabled {
  84. var newRelicCancel context.CancelFunc
  85. ctx, newRelicCancel, rw = startNewRelicTransaction(ctx, rw, r)
  86. defer newRelicCancel()
  87. }
  88. if prometheusEnabled {
  89. prometheusRequestsTotal.Inc()
  90. defer startPrometheusDuration(prometheusRequestDuration)()
  91. }
  92. select {
  93. case processingSem <- struct{}{}:
  94. case <-ctx.Done():
  95. panic(newError(499, "Request was cancelled before processing", "Cancelled"))
  96. }
  97. defer func() { <-processingSem }()
  98. ctx, timeoutCancel := context.WithTimeout(ctx, time.Duration(conf.WriteTimeout)*time.Second)
  99. defer timeoutCancel()
  100. ctx, err := parsePath(ctx, r)
  101. if err != nil {
  102. panic(err)
  103. }
  104. ctx, downloadcancel, err := downloadImage(ctx)
  105. defer downloadcancel()
  106. if err != nil {
  107. if newRelicEnabled {
  108. sendErrorToNewRelic(ctx, err)
  109. }
  110. if prometheusEnabled {
  111. incrementPrometheusErrorsTotal("download")
  112. }
  113. if fallbackImage == nil {
  114. panic(err)
  115. }
  116. if ierr, ok := err.(*imgproxyError); !ok || ierr.Unexpected {
  117. reportError(err, r)
  118. }
  119. logWarning("Could not load image. Using fallback image: %s", err.Error())
  120. ctx = context.WithValue(ctx, imageDataCtxKey, fallbackImage)
  121. }
  122. checkTimeout(ctx)
  123. if conf.ETagEnabled {
  124. eTag := calcETag(ctx)
  125. rw.Header().Set("ETag", eTag)
  126. if eTag == r.Header.Get("If-None-Match") {
  127. respondWithNotModified(ctx, reqID, r, rw)
  128. return
  129. }
  130. }
  131. checkTimeout(ctx)
  132. if len(conf.SkipProcessingFormats) > 0 {
  133. imgdata := getImageData(ctx)
  134. po := getProcessingOptions(ctx)
  135. if imgdata.Type == po.Format || po.Format == imageTypeUnknown {
  136. for _, f := range conf.SkipProcessingFormats {
  137. if f == imgdata.Type {
  138. po.Format = imgdata.Type
  139. respondWithImage(ctx, reqID, r, rw, imgdata.Data)
  140. return
  141. }
  142. }
  143. }
  144. }
  145. imageData, processcancel, err := processImage(ctx)
  146. defer processcancel()
  147. if err != nil {
  148. if newRelicEnabled {
  149. sendErrorToNewRelic(ctx, err)
  150. }
  151. if prometheusEnabled {
  152. incrementPrometheusErrorsTotal("processing")
  153. }
  154. panic(err)
  155. }
  156. checkTimeout(ctx)
  157. respondWithImage(ctx, reqID, r, rw, imageData)
  158. }