request_methods.go 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  1. package processing
  2. import (
  3. "context"
  4. "io"
  5. "net/http"
  6. "strconv"
  7. "github.com/imgproxy/imgproxy/v3/cookies"
  8. "github.com/imgproxy/imgproxy/v3/errorreport"
  9. "github.com/imgproxy/imgproxy/v3/httpheaders"
  10. "github.com/imgproxy/imgproxy/v3/ierrors"
  11. "github.com/imgproxy/imgproxy/v3/imagedata"
  12. "github.com/imgproxy/imgproxy/v3/monitoring"
  13. "github.com/imgproxy/imgproxy/v3/options"
  14. "github.com/imgproxy/imgproxy/v3/processing"
  15. "github.com/imgproxy/imgproxy/v3/server"
  16. log "github.com/sirupsen/logrus"
  17. )
  18. // makeImageRequestHeaders creates headers for the image request
  19. func (r *request) makeImageRequestHeaders() http.Header {
  20. h := make(http.Header)
  21. // If ETag is enabled, we forward If-None-Match header
  22. if r.config.ETagEnabled {
  23. h.Set(httpheaders.IfNoneMatch, r.imageRequest.Header.Get(httpheaders.IfNoneMatch))
  24. }
  25. // If LastModified is enabled, we forward If-Modified-Since header
  26. if r.config.LastModifiedEnabled {
  27. h.Set(httpheaders.IfModifiedSince, r.imageRequest.Header.Get(httpheaders.IfModifiedSince))
  28. }
  29. return h
  30. }
  31. // acquireProcessingSem acquires the processing semaphore
  32. func (r *request) acquireProcessingSem(ctx context.Context) (context.CancelFunc, error) {
  33. defer monitoring.StartQueueSegment(ctx)()
  34. fn, err := r.semaphores.AcquireProcessing(ctx)
  35. if err != nil {
  36. // We don't actually need to check timeout here,
  37. // but it's an easy way to check if this is an actual timeout
  38. // or the request was canceled
  39. if terr := server.CheckTimeout(ctx); terr != nil {
  40. return nil, ierrors.Wrap(terr, 0, ierrors.WithCategory(categoryTimeout))
  41. }
  42. // We should never reach this line as err could be only ctx.Err()
  43. // and we've already checked for it. But beter safe than sorry
  44. return nil, ierrors.Wrap(err, 0, ierrors.WithCategory(categoryQueue))
  45. }
  46. return fn, nil
  47. }
  48. // makeDownloadOptions creates a new default download options
  49. func (r *request) makeDownloadOptions(ctx context.Context, h http.Header) imagedata.DownloadOptions {
  50. downloadFinished := monitoring.StartDownloadingSegment(ctx, r.monitoringMeta.Filter(
  51. monitoring.MetaSourceImageURL,
  52. monitoring.MetaSourceImageOrigin,
  53. ))
  54. return imagedata.DownloadOptions{
  55. Header: h,
  56. MaxSrcFileSize: r.po.SecurityOptions.MaxSrcFileSize,
  57. DownloadFinished: downloadFinished,
  58. }
  59. }
  60. // fetchImage downloads the source image asynchronously
  61. func (r *request) fetchImage(ctx context.Context, do imagedata.DownloadOptions) (imagedata.ImageData, http.Header, error) {
  62. var err error
  63. if r.config.CookiePassthrough {
  64. do.CookieJar, err = cookies.JarFromRequest(r.imageRequest)
  65. if err != nil {
  66. return nil, nil, ierrors.Wrap(err, 0, ierrors.WithCategory(categoryDownload))
  67. }
  68. }
  69. return r.idf.DownloadAsync(ctx, r.imageURL, "source image", do)
  70. }
  71. // handleDownloadError replaces the image data with fallback image if needed
  72. func (r *request) handleDownloadError(
  73. ctx context.Context,
  74. originalErr error,
  75. ) (imagedata.ImageData, int, error) {
  76. err := r.wrapDownloadingErr(originalErr)
  77. // If there is no fallback image configured, just return the error
  78. data, headers := r.getFallbackImage(ctx, r.po)
  79. if data == nil {
  80. return nil, 0, err
  81. }
  82. // Just send error
  83. monitoring.SendError(ctx, categoryDownload, err)
  84. // We didn't return, so we have to report error
  85. if err.ShouldReport() {
  86. errorreport.Report(err, r.imageRequest)
  87. }
  88. log.
  89. WithField("request_id", r.reqID).
  90. Warningf("Could not load image %s. Using fallback image. %s", r.imageURL, err.Error())
  91. var statusCode int
  92. // Set status code if needed
  93. if r.config.FallbackImageHTTPCode > 0 {
  94. statusCode = r.config.FallbackImageHTTPCode
  95. } else {
  96. statusCode = err.StatusCode()
  97. }
  98. // Fallback image should have exact FallbackImageTTL lifetime
  99. headers.Del(httpheaders.Expires)
  100. headers.Del(httpheaders.LastModified)
  101. r.hwr.SetOriginHeaders(headers)
  102. r.hwr.SetIsFallbackImage()
  103. return data, statusCode, nil
  104. }
  105. // getFallbackImage returns fallback image if any
  106. func (r *request) getFallbackImage(
  107. ctx context.Context,
  108. po *options.ProcessingOptions,
  109. ) (imagedata.ImageData, http.Header) {
  110. if r.handler.fallbackImage == nil {
  111. return nil, nil
  112. }
  113. data, h, err := r.handler.fallbackImage.Get(ctx, po)
  114. if err != nil {
  115. log.Warning(err.Error())
  116. if ierr := r.wrapDownloadingErr(err); ierr.ShouldReport() {
  117. errorreport.Report(ierr, r.imageRequest)
  118. }
  119. return nil, nil
  120. }
  121. return data, h
  122. }
  123. // processImage calls actual image processing
  124. func (r *request) processImage(ctx context.Context, originData imagedata.ImageData) (*processing.Result, error) {
  125. defer monitoring.StartProcessingSegment(ctx, r.monitoringMeta.Filter(monitoring.MetaProcessingOptions))()
  126. return processing.ProcessImage(ctx, originData, r.po, r.handler.watermarkImage, r.handler.imageData)
  127. }
  128. // writeDebugHeaders writes debug headers (X-Origin-*, X-Result-*) to the response
  129. func (r *request) writeDebugHeaders(result *processing.Result, originData imagedata.ImageData) error {
  130. if !r.config.EnableDebugHeaders {
  131. return nil
  132. }
  133. if result != nil {
  134. r.rw.Header().Set(httpheaders.XOriginWidth, strconv.Itoa(result.OriginWidth))
  135. r.rw.Header().Set(httpheaders.XOriginHeight, strconv.Itoa(result.OriginHeight))
  136. r.rw.Header().Set(httpheaders.XResultWidth, strconv.Itoa(result.ResultWidth))
  137. r.rw.Header().Set(httpheaders.XResultHeight, strconv.Itoa(result.ResultHeight))
  138. }
  139. // Try to read origin image size
  140. size, err := originData.Size()
  141. if err != nil {
  142. return ierrors.Wrap(err, 0, ierrors.WithCategory(categoryImageDataSize))
  143. }
  144. r.rw.Header().Set(httpheaders.XOriginContentLength, strconv.Itoa(size))
  145. return nil
  146. }
  147. // respondWithNotModified writes not-modified response
  148. func (r *request) respondWithNotModified() error {
  149. r.hwr.SetExpires(r.po.Expires)
  150. r.hwr.SetVary()
  151. if r.config.LastModifiedEnabled {
  152. r.hwr.Passthrough(httpheaders.LastModified)
  153. }
  154. if r.config.ETagEnabled {
  155. r.hwr.Passthrough(httpheaders.Etag)
  156. }
  157. r.hwr.Write(r.rw)
  158. r.rw.WriteHeader(http.StatusNotModified)
  159. server.LogResponse(
  160. r.reqID, r.imageRequest, http.StatusNotModified, nil,
  161. log.Fields{
  162. "image_url": r.imageURL,
  163. "processing_options": r.po,
  164. },
  165. )
  166. return nil
  167. }
  168. func (r *request) respondWithImage(statusCode int, resultData imagedata.ImageData) error {
  169. // We read the size of the image data here, so we can set Content-Length header.
  170. // This indireclty ensures that the image data is fully read from the source, no
  171. // errors happened.
  172. resultSize, err := resultData.Size()
  173. if err != nil {
  174. return ierrors.Wrap(err, 0, ierrors.WithCategory(categoryImageDataSize))
  175. }
  176. r.hwr.SetContentType(resultData.Format().Mime())
  177. r.hwr.SetContentLength(resultSize)
  178. r.hwr.SetContentDisposition(
  179. r.imageURL,
  180. r.po.Filename,
  181. resultData.Format().Ext(),
  182. "",
  183. r.po.ReturnAttachment,
  184. )
  185. r.hwr.SetExpires(r.po.Expires)
  186. r.hwr.SetVary()
  187. r.hwr.SetCanonical(r.imageURL)
  188. if r.config.LastModifiedEnabled {
  189. r.hwr.Passthrough(httpheaders.LastModified)
  190. }
  191. if r.config.ETagEnabled {
  192. r.hwr.Passthrough(httpheaders.Etag)
  193. }
  194. r.hwr.Write(r.rw)
  195. r.rw.WriteHeader(statusCode)
  196. _, err = io.Copy(r.rw, resultData.Reader())
  197. var ierr *ierrors.Error
  198. if err != nil {
  199. ierr = newResponseWriteError(err)
  200. if r.config.ReportIOErrors {
  201. return ierrors.Wrap(ierr, 0, ierrors.WithCategory(categoryIO), ierrors.WithShouldReport(true))
  202. }
  203. }
  204. server.LogResponse(
  205. r.reqID, r.imageRequest, statusCode, ierr,
  206. log.Fields{
  207. "image_url": r.imageURL,
  208. "processing_options": r.po,
  209. },
  210. )
  211. return nil
  212. }
  213. // wrapDownloadingErr wraps original error to download error
  214. func (r *request) wrapDownloadingErr(originalErr error) *ierrors.Error {
  215. err := ierrors.Wrap(originalErr, 0, ierrors.WithCategory(categoryDownload))
  216. // we report this error only if enabled
  217. if r.config.ReportDownloadingErrors {
  218. err = ierrors.Wrap(err, 0, ierrors.WithShouldReport(true))
  219. }
  220. return err
  221. }