1
0

request_methods.go 7.6 KB

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