processing_handler.go -> handlers/processing

This commit is contained in:
Viktor Sokolov
2025-08-26 16:19:41 +02:00
committed by Sergei Aleksandrovich
parent 7aec46f146
commit 8bc70491fb
30 changed files with 1489 additions and 546 deletions

View File

@@ -0,0 +1,59 @@
package processing
import (
"errors"
"net/http"
"github.com/imgproxy/imgproxy/v3/config"
)
// Config represents handler config
type Config struct {
PathPrefix string // Route path prefix
CookiePassthrough bool // Whether to passthrough cookies
ReportDownloadingErrors bool // Whether to report downloading errors
LastModifiedEnabled bool // Whether to enable Last-Modified
ETagEnabled bool // Whether to enable ETag
ReportIOErrors bool // Whether to report IO errors
FallbackImageHTTPCode int // Fallback image HTTP status code
EnableDebugHeaders bool // Whether to enable debug headers
FallbackImageData string // Fallback image data (base64)
FallbackImagePath string // Fallback image path (local file system)
FallbackImageURL string // Fallback image URL (remote)
}
// NewDefaultConfig creates a new configuration with defaults
func NewDefaultConfig() *Config {
return &Config{
PathPrefix: "",
CookiePassthrough: false,
ReportDownloadingErrors: true,
LastModifiedEnabled: true,
ETagEnabled: true,
ReportIOErrors: false,
FallbackImageHTTPCode: http.StatusOK,
EnableDebugHeaders: false,
}
}
// LoadFromEnv loads config from environment variables
func LoadFromEnv(c *Config) (*Config, error) {
c.PathPrefix = config.PathPrefix
c.CookiePassthrough = config.CookiePassthrough
c.ReportDownloadingErrors = config.ReportDownloadingErrors
c.LastModifiedEnabled = config.LastModifiedEnabled
c.ETagEnabled = config.ETagEnabled
c.ReportIOErrors = config.ReportIOErrors
c.FallbackImageHTTPCode = config.FallbackImageHTTPCode
c.EnableDebugHeaders = config.EnableDebugHeaders
return c, nil
}
// Validate checks configuration values
func (c *Config) Validate() error {
if c.FallbackImageHTTPCode != 0 && (c.FallbackImageHTTPCode < 100 || c.FallbackImageHTTPCode > 599) {
return errors.New("fallback image HTTP code should be between 100 and 599")
}
return nil
}

View File

@@ -0,0 +1,71 @@
package processing
import (
"fmt"
"net/http"
"github.com/imgproxy/imgproxy/v3/ierrors"
"github.com/imgproxy/imgproxy/v3/imagetype"
)
// Monitoring error categories
const (
categoryTimeout = "timeout"
categoryImageDataSize = "image_data_size"
categoryPathParsing = "path_parsing"
categorySecurity = "security"
categoryQueue = "queue"
categoryDownload = "download"
categoryProcessing = "processing"
categoryIO = "IO"
categoryConfig = "config(tmp)" // NOTE: THIS IS TEMPORARY
)
type (
ResponseWriteError struct{ error }
InvalidURLError string
)
func newResponseWriteError(cause error) *ierrors.Error {
return ierrors.Wrap(
ResponseWriteError{cause},
1,
ierrors.WithPublicMessage("Failed to write response"),
)
}
func (e ResponseWriteError) Error() string {
return fmt.Sprintf("Failed to write response: %s", e.error)
}
func (e ResponseWriteError) Unwrap() error {
return e.error
}
func newInvalidURLErrorf(status int, format string, args ...interface{}) error {
return ierrors.Wrap(
InvalidURLError(fmt.Sprintf(format, args...)),
1,
ierrors.WithStatusCode(status),
ierrors.WithPublicMessage("Invalid URL"),
ierrors.WithShouldReport(false),
)
}
func (e InvalidURLError) Error() string { return string(e) }
// newCantSaveError creates "resulting image not supported" error
func newCantSaveError(format imagetype.Type) error {
return ierrors.Wrap(newInvalidURLErrorf(
http.StatusUnprocessableEntity,
"Resulting image format is not supported: %s", format,
), 1, ierrors.WithCategory(categoryPathParsing))
}
// newCantLoadError creates "source image not supported" error
func newCantLoadError(format imagetype.Type) error {
return ierrors.Wrap(newInvalidURLErrorf(
http.StatusUnprocessableEntity,
"Source image format is not supported: %s", format,
), 1, ierrors.WithCategory(categoryProcessing))
}

View File

@@ -0,0 +1,143 @@
package processing
import (
"context"
"net/http"
"net/url"
"github.com/imgproxy/imgproxy/v3/auximageprovider"
"github.com/imgproxy/imgproxy/v3/errorreport"
"github.com/imgproxy/imgproxy/v3/handlers/stream"
"github.com/imgproxy/imgproxy/v3/headerwriter"
"github.com/imgproxy/imgproxy/v3/ierrors"
"github.com/imgproxy/imgproxy/v3/monitoring"
"github.com/imgproxy/imgproxy/v3/monitoring/stats"
"github.com/imgproxy/imgproxy/v3/options"
"github.com/imgproxy/imgproxy/v3/security"
"github.com/imgproxy/imgproxy/v3/semaphores"
)
// Handler handles image processing requests
type Handler struct {
hw *headerwriter.Writer // Configured HeaderWriter instance
stream *stream.Handler // Stream handler for raw image streaming
config *Config // Handler configuration
semaphores *semaphores.Semaphores
fallbackImage auximageprovider.Provider
}
// New creates new handler object
func New(
stream *stream.Handler,
hw *headerwriter.Writer,
semaphores *semaphores.Semaphores,
fi auximageprovider.Provider,
config *Config,
) (*Handler, error) {
if err := config.Validate(); err != nil {
return nil, err
}
return &Handler{
hw: hw,
config: config,
stream: stream,
semaphores: semaphores,
fallbackImage: fi,
}, nil
}
// Execute handles the image processing request
func (h *Handler) Execute(
reqID string,
rw http.ResponseWriter,
imageRequest *http.Request,
) error {
// Increment the number of requests in progress
stats.IncRequestsInProgress()
defer stats.DecRequestsInProgress()
ctx := imageRequest.Context()
// Verify URL signature and extract image url and processing options
imageURL, po, mm, err := h.newRequest(ctx, imageRequest)
if err != nil {
return err
}
// if processing options indicate raw image streaming, stream it and return
if po.Raw {
return h.stream.Execute(ctx, imageRequest, imageURL, reqID, po, rw)
}
req := &request{
handler: h,
imageRequest: imageRequest,
reqID: reqID,
rw: rw,
config: h.config,
po: po,
imageURL: imageURL,
monitoringMeta: mm,
semaphores: h.semaphores,
hwr: h.hw.NewRequest(),
}
return req.execute(ctx)
}
// newRequest extracts image url and processing options from request URL and verifies them
func (h *Handler) newRequest(
ctx context.Context,
imageRequest *http.Request,
) (string, *options.ProcessingOptions, monitoring.Meta, error) {
// let's extract signature and valid request path from a request
path, signature, err := splitPathSignature(imageRequest, h.config)
if err != nil {
return "", nil, nil, err
}
// verify the signature (if any)
if err = security.VerifySignature(signature, path); err != nil {
return "", nil, nil, ierrors.Wrap(err, 0, ierrors.WithCategory(categorySecurity))
}
// parse image url and processing options
po, imageURL, err := options.ParsePath(path, imageRequest.Header)
if err != nil {
return "", nil, nil, ierrors.Wrap(err, 0, ierrors.WithCategory(categoryPathParsing))
}
// get image origin and create monitoring meta object
imageOrigin := imageOrigin(imageURL)
mm := monitoring.Meta{
monitoring.MetaSourceImageURL: imageURL,
monitoring.MetaSourceImageOrigin: imageOrigin,
monitoring.MetaProcessingOptions: po.Diff().Flatten(),
}
// set error reporting and monitoring context
errorreport.SetMetadata(imageRequest, "Source Image URL", imageURL)
errorreport.SetMetadata(imageRequest, "Source Image Origin", imageOrigin)
errorreport.SetMetadata(imageRequest, "Processing Options", po)
monitoring.SetMetadata(ctx, mm)
// verify that image URL came from the valid source
err = security.VerifySourceURL(imageURL)
if err != nil {
return "", nil, mm, ierrors.Wrap(err, 0, ierrors.WithCategory(categorySecurity))
}
return imageURL, po, mm, nil
}
// imageOrigin extracts image origin from URL
func imageOrigin(imageURL string) string {
if u, uerr := url.Parse(imageURL); uerr == nil {
return u.Scheme + "://" + u.Host
}
return ""
}

View File

@@ -0,0 +1,56 @@
package processing
import (
"fmt"
"net/http"
"regexp"
"strings"
"github.com/imgproxy/imgproxy/v3/ierrors"
)
// fixPathRe is used in path re-denormalization
var fixPathRe = regexp.MustCompile(`/plain/(\S+)\:/([^/])`)
// splitPathSignature splits signature and path components from the request URI
func splitPathSignature(r *http.Request, config *Config) (string, string, error) {
uri := r.RequestURI
// cut query params
uri, _, _ = strings.Cut(uri, "?")
// cut path prefix
if len(config.PathPrefix) > 0 {
uri = strings.TrimPrefix(uri, config.PathPrefix)
}
// cut leading slash
uri = strings.TrimPrefix(uri, "/")
signature, path, _ := strings.Cut(uri, "/")
if len(signature) == 0 || len(path) == 0 {
return "", "", ierrors.Wrap(
newInvalidURLErrorf(http.StatusNotFound, "Invalid path: %s", path), 0,
ierrors.WithCategory(categoryPathParsing),
)
}
// restore broken slashes in the path
path = redenormalizePath(path)
return path, signature, nil
}
// redenormalizePath undoes path normalization done by some browsers and revers proxies
func redenormalizePath(path string) string {
for _, match := range fixPathRe.FindAllStringSubmatch(path, -1) {
repl := fmt.Sprintf("/plain/%s://", match[1])
if match[1] == "local" {
repl += "/"
}
repl += match[2]
path = strings.Replace(path, match[0], repl, 1)
}
return path
}

View File

@@ -0,0 +1,180 @@
package processing
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/imgproxy/imgproxy/v3/ierrors"
"github.com/stretchr/testify/suite"
)
type PathTestSuite struct {
suite.Suite
}
func TestPathTestSuite(t *testing.T) {
suite.Run(t, new(PathTestSuite))
}
func (s *PathTestSuite) createRequest(path string) *http.Request {
return httptest.NewRequest("GET", path, nil)
}
func (s *PathTestSuite) TestParsePath() {
testCases := []struct {
name string
pathPrefix string
requestPath string
expectedPath string
expectedSig string
expectedError bool
}{
{
name: "BasicPath",
requestPath: "/dummy_signature/rs:fill:300:200/plain/http://example.com/image.jpg",
expectedPath: "rs:fill:300:200/plain/http://example.com/image.jpg",
expectedSig: "dummy_signature",
expectedError: false,
},
{
name: "PathWithQueryParams",
requestPath: "/dummy_signature/rs:fill:300:200/plain/http://example.com/image.jpg?param1=value1&param2=value2",
expectedPath: "rs:fill:300:200/plain/http://example.com/image.jpg",
expectedSig: "dummy_signature",
expectedError: false,
},
{
name: "PathWithPrefix",
pathPrefix: "/imgproxy",
requestPath: "/imgproxy/dummy_signature/rs:fill:300:200/plain/http://example.com/image.jpg",
expectedPath: "rs:fill:300:200/plain/http://example.com/image.jpg",
expectedSig: "dummy_signature",
expectedError: false,
},
{
name: "PathWithRedenormalization",
requestPath: "/dummy_signature/rs:fill:300:200/plain/https:/example.com/path/to/image.jpg",
expectedPath: "rs:fill:300:200/plain/https://example.com/path/to/image.jpg",
expectedSig: "dummy_signature",
expectedError: false,
},
{
name: "NoSignatureSeparator",
requestPath: "/invalid_path_without_slash",
expectedPath: "",
expectedSig: "",
expectedError: true,
},
{
name: "EmptyPath",
requestPath: "/",
expectedPath: "",
expectedSig: "",
expectedError: true,
},
{
name: "OnlySignature",
requestPath: "/signature_only",
expectedPath: "",
expectedSig: "",
expectedError: true,
},
}
for _, tc := range testCases {
s.Run(tc.name, func() {
config := &Config{
PathPrefix: tc.pathPrefix,
}
req := s.createRequest(tc.requestPath)
path, signature, err := splitPathSignature(req, config)
if tc.expectedError {
var ierr *ierrors.Error
s.Require().Error(err)
s.Require().ErrorAs(err, &ierr)
s.Require().Equal(categoryPathParsing, ierr.Category())
return
}
s.Require().NoError(err)
s.Require().Equal(tc.expectedPath, path)
s.Require().Equal(tc.expectedSig, signature)
})
}
}
func (s *PathTestSuite) TestRedenormalizePathHTTPProtocol() {
testCases := []struct {
name string
input string
expected string
}{
{
name: "HTTP",
input: "/plain/http:/example.com/image.jpg",
expected: "/plain/http://example.com/image.jpg",
},
{
name: "HTTPS",
input: "/plain/https:/example.com/image.jpg",
expected: "/plain/https://example.com/image.jpg",
},
{
name: "Local",
input: "/plain/local:/image.jpg",
expected: "/plain/local:///image.jpg",
},
{
name: "NormalizedPath",
input: "/plain/http://example.com/image.jpg",
expected: "/plain/http://example.com/image.jpg",
},
{
name: "ProtocolMissing",
input: "/rs:fill:300:200/plain/example.com/image.jpg",
expected: "/rs:fill:300:200/plain/example.com/image.jpg",
},
{
name: "EmptyString",
input: "",
expected: "",
},
{
name: "SingleSlash",
input: "/",
expected: "/",
},
{
name: "NoPlainPrefix",
input: "/http:/example.com/image.jpg",
expected: "/http:/example.com/image.jpg",
},
{
name: "NoProtocol",
input: "/plain/example.com/image.jpg",
expected: "/plain/example.com/image.jpg",
},
{
name: "EndsWithProtocol",
input: "/plain/http:",
expected: "/plain/http:",
},
{
name: "OnlyProtocol",
input: "/plain/http:/test",
expected: "/plain/http://test",
},
}
for _, tc := range testCases {
s.Run(tc.name, func() {
result := redenormalizePath(tc.input)
s.Equal(tc.expected, result)
})
}
}

View File

@@ -0,0 +1,140 @@
package processing
import (
"context"
"errors"
"net/http"
"github.com/imgproxy/imgproxy/v3/headerwriter"
"github.com/imgproxy/imgproxy/v3/ierrors"
"github.com/imgproxy/imgproxy/v3/imagefetcher"
"github.com/imgproxy/imgproxy/v3/imagetype"
"github.com/imgproxy/imgproxy/v3/monitoring"
"github.com/imgproxy/imgproxy/v3/monitoring/stats"
"github.com/imgproxy/imgproxy/v3/options"
"github.com/imgproxy/imgproxy/v3/semaphores"
"github.com/imgproxy/imgproxy/v3/server"
"github.com/imgproxy/imgproxy/v3/vips"
)
// request holds the parameters and state for a single request request
type request struct {
handler *Handler
imageRequest *http.Request
reqID string
rw http.ResponseWriter
config *Config
po *options.ProcessingOptions
imageURL string
monitoringMeta monitoring.Meta
semaphores *semaphores.Semaphores
hwr *headerwriter.Request
}
// execute handles the actual processing logic
func (r *request) execute(ctx context.Context) error {
// Check if we can save the resulting image
canSave := vips.SupportsSave(r.po.Format) ||
r.po.Format == imagetype.Unknown ||
r.po.Format == imagetype.SVG
if !canSave {
return newCantSaveError(r.po.Format)
}
// Acquire queue semaphore (if enabled)
releaseQueueSem, err := r.semaphores.AcquireQueue()
if err != nil {
return err
}
defer releaseQueueSem()
// Acquire processing semaphore
releaseProcessingSem, err := r.acquireProcessingSem(ctx)
if err != nil {
return err
}
defer releaseProcessingSem()
// Deal with processing image counter
stats.IncImagesInProgress()
defer stats.DecImagesInProgress()
// Response status code is OK by default
statusCode := http.StatusOK
// Request headers
imgRequestHeaders := r.makeImageRequestHeaders()
// create download options
do := r.makeDownloadOptions(ctx, imgRequestHeaders)
// Fetch image actual
originData, originHeaders, err := r.fetchImage(ctx, do)
if err == nil {
defer originData.Close() // if any originData has been opened, we need to close it
}
// Check that image detection didn't take too long
if terr := server.CheckTimeout(ctx); terr != nil {
return ierrors.Wrap(terr, 0, ierrors.WithCategory(categoryTimeout))
}
// Respond with NotModified if image was not modified
var nmErr imagefetcher.NotModifiedError
if errors.As(err, &nmErr) {
r.hwr.SetOriginHeaders(nmErr.Headers())
return r.respondWithNotModified()
}
// Prepare to write image response headers
r.hwr.SetOriginHeaders(originHeaders)
// If error is not related to NotModified, respond with fallback image and replace image data
if err != nil {
originData, statusCode, err = r.handleDownloadError(ctx, err)
if err != nil {
return err
}
}
// Check if image supports load from origin format
if !vips.SupportsLoad(originData.Format()) {
return newCantLoadError(originData.Format())
}
// Actually process the image
result, err := r.processImage(ctx, originData)
// Let's close resulting image data only if it differs from the source image data
if result != nil && result.OutData != nil && result.OutData != originData {
defer result.OutData.Close()
}
// First, check if the processing error wasn't caused by an image data error
if derr := originData.Error(); derr != nil {
return ierrors.Wrap(derr, 0, ierrors.WithCategory(categoryDownload))
}
// If it wasn't, than it was a processing error
if err != nil {
return ierrors.Wrap(err, 0, ierrors.WithCategory(categoryProcessing))
}
// Write debug headers. It seems unlogical to move they to headerwriter since they're
// not used anywhere else.
err = r.writeDebugHeaders(result, originData)
if err != nil {
return ierrors.Wrap(err, 0, ierrors.WithCategory(categoryImageDataSize))
}
// Responde with actual image
err = r.respondWithImage(statusCode, result.OutData)
if err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,278 @@
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 imagedata.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)
}
// 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
}