request_methods.go 7.6 KB

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