Files
imgproxy/handlers/processing/request_methods.go
Victor Sokolov 37a6ea2bf7 IMG-49: Get rid of global watermark image and fetcher (#1515)
* Get rid of global watermark image

* imagedata.Factory, removed global Fetcher
2025-09-05 16:22:57 +02:00

279 lines
7.7 KiB
Go

package processing
import (
"context"
"io"
"net/http"
"strconv"
"github.com/imgproxy/imgproxy/v3/cookies"
"github.com/imgproxy/imgproxy/v3/errorreport"
"github.com/imgproxy/imgproxy/v3/httpheaders"
"github.com/imgproxy/imgproxy/v3/ierrors"
"github.com/imgproxy/imgproxy/v3/imagedata"
"github.com/imgproxy/imgproxy/v3/monitoring"
"github.com/imgproxy/imgproxy/v3/options"
"github.com/imgproxy/imgproxy/v3/processing"
"github.com/imgproxy/imgproxy/v3/server"
log "github.com/sirupsen/logrus"
)
// makeImageRequestHeaders creates headers for the image request
func (r *request) makeImageRequestHeaders() http.Header {
h := make(http.Header)
// If ETag is enabled, we forward If-None-Match header
if r.config.ETagEnabled {
h.Set(httpheaders.IfNoneMatch, r.imageRequest.Header.Get(httpheaders.IfNoneMatch))
}
// If LastModified is enabled, we forward If-Modified-Since header
if r.config.LastModifiedEnabled {
h.Set(httpheaders.IfModifiedSince, r.imageRequest.Header.Get(httpheaders.IfModifiedSince))
}
return h
}
// acquireProcessingSem acquires the processing semaphore
func (r *request) acquireProcessingSem(ctx context.Context) (context.CancelFunc, error) {
defer monitoring.StartQueueSegment(ctx)()
fn, err := r.semaphores.AcquireProcessing(ctx)
if err != nil {
// We don't actually need to check timeout here,
// but it's an easy way to check if this is an actual timeout
// or the request was canceled
if terr := server.CheckTimeout(ctx); terr != nil {
return nil, ierrors.Wrap(terr, 0, ierrors.WithCategory(categoryTimeout))
}
// We should never reach this line as err could be only ctx.Err()
// and we've already checked for it. But beter safe than sorry
return nil, ierrors.Wrap(err, 0, ierrors.WithCategory(categoryQueue))
}
return fn, nil
}
// makeDownloadOptions creates a new default download options
func (r *request) makeDownloadOptions(ctx context.Context, h http.Header) imagedata.DownloadOptions {
downloadFinished := monitoring.StartDownloadingSegment(ctx, r.monitoringMeta.Filter(
monitoring.MetaSourceImageURL,
monitoring.MetaSourceImageOrigin,
))
return imagedata.DownloadOptions{
Header: h,
MaxSrcFileSize: r.po.SecurityOptions.MaxSrcFileSize,
DownloadFinished: downloadFinished,
}
}
// fetchImage downloads the source image asynchronously
func (r *request) fetchImage(ctx context.Context, do imagedata.DownloadOptions) (imagedata.ImageData, http.Header, error) {
var err error
if r.config.CookiePassthrough {
do.CookieJar, err = cookies.JarFromRequest(r.imageRequest)
if err != nil {
return nil, nil, ierrors.Wrap(err, 0, ierrors.WithCategory(categoryDownload))
}
}
return r.idf.DownloadAsync(ctx, r.imageURL, "source image", do)
}
// handleDownloadError replaces the image data with fallback image if needed
func (r *request) handleDownloadError(
ctx context.Context,
originalErr error,
) (imagedata.ImageData, int, error) {
err := r.wrapDownloadingErr(originalErr)
// If there is no fallback image configured, just return the error
data, headers := r.getFallbackImage(ctx, r.po)
if data == nil {
return nil, 0, err
}
// Just send error
monitoring.SendError(ctx, categoryDownload, err)
// We didn't return, so we have to report error
if err.ShouldReport() {
errorreport.Report(err, r.imageRequest)
}
log.
WithField("request_id", r.reqID).
Warningf("Could not load image %s. Using fallback image. %s", r.imageURL, err.Error())
var statusCode int
// Set status code if needed
if r.config.FallbackImageHTTPCode > 0 {
statusCode = r.config.FallbackImageHTTPCode
} else {
statusCode = err.StatusCode()
}
// Fallback image should have exact FallbackImageTTL lifetime
headers.Del(httpheaders.Expires)
headers.Del(httpheaders.LastModified)
r.hwr.SetOriginHeaders(headers)
r.hwr.SetIsFallbackImage()
return data, statusCode, nil
}
// getFallbackImage returns fallback image if any
func (r *request) getFallbackImage(
ctx context.Context,
po *options.ProcessingOptions,
) (imagedata.ImageData, http.Header) {
if r.handler.fallbackImage == nil {
return nil, nil
}
data, h, err := r.handler.fallbackImage.Get(ctx, po)
if err != nil {
log.Warning(err.Error())
if ierr := r.wrapDownloadingErr(err); ierr.ShouldReport() {
errorreport.Report(ierr, r.imageRequest)
}
return nil, nil
}
return data, h
}
// processImage calls actual image processing
func (r *request) processImage(ctx context.Context, originData imagedata.ImageData) (*processing.Result, error) {
defer monitoring.StartProcessingSegment(ctx, r.monitoringMeta.Filter(monitoring.MetaProcessingOptions))()
return processing.ProcessImage(ctx, originData, r.po, r.handler.watermarkImage, r.handler.imageData)
}
// writeDebugHeaders writes debug headers (X-Origin-*, X-Result-*) to the response
func (r *request) writeDebugHeaders(result *processing.Result, originData imagedata.ImageData) error {
if !r.config.EnableDebugHeaders {
return nil
}
if result != nil {
r.rw.Header().Set(httpheaders.XOriginWidth, strconv.Itoa(result.OriginWidth))
r.rw.Header().Set(httpheaders.XOriginHeight, strconv.Itoa(result.OriginHeight))
r.rw.Header().Set(httpheaders.XResultWidth, strconv.Itoa(result.ResultWidth))
r.rw.Header().Set(httpheaders.XResultHeight, strconv.Itoa(result.ResultHeight))
}
// Try to read origin image size
size, err := originData.Size()
if err != nil {
return ierrors.Wrap(err, 0, ierrors.WithCategory(categoryImageDataSize))
}
r.rw.Header().Set(httpheaders.XOriginContentLength, strconv.Itoa(size))
return nil
}
// respondWithNotModified writes not-modified response
func (r *request) respondWithNotModified() error {
r.hwr.SetExpires(r.po.Expires)
r.hwr.SetVary()
if r.config.LastModifiedEnabled {
r.hwr.Passthrough(httpheaders.LastModified)
}
if r.config.ETagEnabled {
r.hwr.Passthrough(httpheaders.Etag)
}
r.hwr.Write(r.rw)
r.rw.WriteHeader(http.StatusNotModified)
server.LogResponse(
r.reqID, r.imageRequest, http.StatusNotModified, nil,
log.Fields{
"image_url": r.imageURL,
"processing_options": r.po,
},
)
return nil
}
func (r *request) respondWithImage(statusCode int, resultData imagedata.ImageData) error {
// We read the size of the image data here, so we can set Content-Length header.
// This indireclty ensures that the image data is fully read from the source, no
// errors happened.
resultSize, err := resultData.Size()
if err != nil {
return ierrors.Wrap(err, 0, ierrors.WithCategory(categoryImageDataSize))
}
r.hwr.SetContentType(resultData.Format().Mime())
r.hwr.SetContentLength(resultSize)
r.hwr.SetContentDisposition(
r.imageURL,
r.po.Filename,
resultData.Format().Ext(),
"",
r.po.ReturnAttachment,
)
r.hwr.SetExpires(r.po.Expires)
r.hwr.SetVary()
r.hwr.SetCanonical(r.imageURL)
if r.config.LastModifiedEnabled {
r.hwr.Passthrough(httpheaders.LastModified)
}
if r.config.ETagEnabled {
r.hwr.Passthrough(httpheaders.Etag)
}
r.hwr.Write(r.rw)
r.rw.WriteHeader(statusCode)
_, err = io.Copy(r.rw, resultData.Reader())
var ierr *ierrors.Error
if err != nil {
ierr = newResponseWriteError(err)
if r.config.ReportIOErrors {
return ierrors.Wrap(ierr, 0, ierrors.WithCategory(categoryIO), ierrors.WithShouldReport(true))
}
}
server.LogResponse(
r.reqID, r.imageRequest, statusCode, ierr,
log.Fields{
"image_url": r.imageURL,
"processing_options": r.po,
},
)
return nil
}
// wrapDownloadingErr wraps original error to download error
func (r *request) wrapDownloadingErr(originalErr error) *ierrors.Error {
err := ierrors.Wrap(originalErr, 0, ierrors.WithCategory(categoryDownload))
// we report this error only if enabled
if r.config.ReportDownloadingErrors {
err = ierrors.Wrap(err, 0, ierrors.WithShouldReport(true))
}
return err
}