Sfoglia il codice sorgente

IMG-13: separate http.Headers from ImageData (#1475)

* Separate headers from ImageData

* processing.Result
Victor Sokolov 2 mesi fa
parent
commit
f7a13c99de

+ 3 - 2
etag/etag.go

@@ -7,6 +7,7 @@ import (
 	"fmt"
 	"hash"
 	"io"
+	"net/http"
 	"net/textproto"
 	"strings"
 	"sync"
@@ -107,9 +108,9 @@ func (h *Handler) ImageEtagExpected() string {
 	return h.imgEtagExpected
 }
 
-func (h *Handler) SetActualImageData(imgdata imagedata.ImageData) (bool, error) {
+func (h *Handler) SetActualImageData(imgdata imagedata.ImageData, headers http.Header) (bool, error) {
 	var haveActualImgETag bool
-	h.imgEtagActual = imgdata.Headers().Get(httpheaders.Etag)
+	h.imgEtagActual = headers.Get(httpheaders.Etag)
 	haveActualImgETag = len(h.imgEtagActual) > 0
 
 	// Just in case server didn't check ETag properly and returned the same one

+ 18 - 13
etag/etag_test.go

@@ -2,6 +2,7 @@ package etag
 
 import (
 	"io"
+	"net/http"
 	"os"
 	"strings"
 	"testing"
@@ -23,9 +24,11 @@ const (
 type EtagTestSuite struct {
 	suite.Suite
 
-	po             *options.ProcessingOptions
-	imgWithETag    imagedata.ImageData
-	imgWithoutETag imagedata.ImageData
+	po                    *options.ProcessingOptions
+	imgWithETag           imagedata.ImageData
+	imgWithEtagHeaders    http.Header
+	imgWithoutETag        imagedata.ImageData
+	imgWithoutEtagHeaders http.Header
 
 	h Handler
 }
@@ -39,9 +42,11 @@ func (s *EtagTestSuite) SetupSuite() {
 
 	imgWithETag, err := imagedata.NewFromBytes(d)
 	s.Require().NoError(err)
-	imgWithETag.Headers().Add(httpheaders.Etag, `"loremipsumdolor"`)
+	s.imgWithEtagHeaders = make(http.Header)
+	s.imgWithEtagHeaders.Add(httpheaders.Etag, `"loremipsumdolor"`)
 
 	imgWithoutETag, err := imagedata.NewFromBytes(d)
+	s.imgWithoutEtagHeaders = make(http.Header)
 	s.Require().NoError(err)
 
 	s.imgWithETag = imgWithETag
@@ -59,14 +64,14 @@ func (s *EtagTestSuite) SetupTest() {
 
 func (s *EtagTestSuite) TestGenerateActualReq() {
 	s.h.SetActualProcessingOptions(s.po)
-	s.h.SetActualImageData(s.imgWithETag)
+	s.h.SetActualImageData(s.imgWithETag, s.imgWithEtagHeaders)
 
 	s.Require().Equal(etagReq, s.h.GenerateActualETag())
 }
 
 func (s *EtagTestSuite) TestGenerateActualData() {
 	s.h.SetActualProcessingOptions(s.po)
-	s.h.SetActualImageData(s.imgWithoutETag)
+	s.h.SetActualImageData(s.imgWithoutETag, s.imgWithoutEtagHeaders)
 
 	s.Require().Equal(etagData, s.h.GenerateActualETag())
 }
@@ -102,7 +107,7 @@ func (s *EtagTestSuite) TestImageETagExpectedPresent() {
 	s.h.ParseExpectedETag(etagReq)
 
 	//nolint:testifylint // False-positive expected-actual
-	s.Require().Equal(s.imgWithETag.Headers().Get(httpheaders.Etag), s.h.ImageEtagExpected())
+	s.Require().Equal(s.imgWithEtagHeaders.Get(httpheaders.Etag), s.h.ImageEtagExpected())
 }
 
 func (s *EtagTestSuite) TestImageETagExpectedBlank() {
@@ -113,7 +118,7 @@ func (s *EtagTestSuite) TestImageETagExpectedBlank() {
 
 func (s *EtagTestSuite) TestImageDataCheckDataToDataSuccess() {
 	s.h.ParseExpectedETag(etagData)
-	s.Require().True(s.h.SetActualImageData(s.imgWithoutETag))
+	s.Require().True(s.h.SetActualImageData(s.imgWithoutETag, s.imgWithoutEtagHeaders))
 }
 
 func (s *EtagTestSuite) TestImageDataCheckDataToDataFailure() {
@@ -121,12 +126,12 @@ func (s *EtagTestSuite) TestImageDataCheckDataToDataFailure() {
 	wrongEtag := etagData[:i] + `/Dwrongimghash"`
 
 	s.h.ParseExpectedETag(wrongEtag)
-	s.Require().False(s.h.SetActualImageData(s.imgWithoutETag))
+	s.Require().False(s.h.SetActualImageData(s.imgWithoutETag, s.imgWithoutEtagHeaders))
 }
 
 func (s *EtagTestSuite) TestImageDataCheckDataToReqSuccess() {
 	s.h.ParseExpectedETag(etagData)
-	s.Require().True(s.h.SetActualImageData(s.imgWithETag))
+	s.Require().True(s.h.SetActualImageData(s.imgWithETag, s.imgWithEtagHeaders))
 }
 
 func (s *EtagTestSuite) TestImageDataCheckDataToReqFailure() {
@@ -134,19 +139,19 @@ func (s *EtagTestSuite) TestImageDataCheckDataToReqFailure() {
 	wrongEtag := etagData[:i] + `/Dwrongimghash"`
 
 	s.h.ParseExpectedETag(wrongEtag)
-	s.Require().False(s.h.SetActualImageData(s.imgWithETag))
+	s.Require().False(s.h.SetActualImageData(s.imgWithETag, s.imgWithEtagHeaders))
 }
 
 func (s *EtagTestSuite) TestImageDataCheckReqToDataFailure() {
 	s.h.ParseExpectedETag(etagReq)
-	s.Require().False(s.h.SetActualImageData(s.imgWithoutETag))
+	s.Require().False(s.h.SetActualImageData(s.imgWithoutETag, s.imgWithoutEtagHeaders))
 }
 
 func (s *EtagTestSuite) TestETagBusterFailure() {
 	config.ETagBuster = "busted"
 
 	s.h.ParseExpectedETag(etagReq)
-	s.Require().False(s.h.SetActualImageData(s.imgWithoutETag))
+	s.Require().False(s.h.SetActualImageData(s.imgWithoutETag, s.imgWithoutEtagHeaders))
 }
 
 func TestEtag(t *testing.T) {

+ 5 - 0
httpheaders/headers.go

@@ -61,4 +61,9 @@ const (
 	XForwardedHost                  = "X-Forwarded-Host"
 	XForwardedProto                 = "X-Forwarded-Proto"
 	XFrameOptions                   = "X-Frame-Options"
+	XOriginWidth                    = "X-Origin-Width"
+	XOriginHeight                   = "X-Origin-Height"
+	XResultWidth                    = "X-Result-Width"
+	XResultHeight                   = "X-Result-Height"
+	XOriginContentLength            = "X-Origin-Content-Length"
 )

+ 11 - 13
imagedata/download.go

@@ -37,7 +37,9 @@ func initDownloading() error {
 	return nil
 }
 
-func download(ctx context.Context, imageURL string, opts DownloadOptions, secopts security.Options) (ImageData, error) {
+func download(ctx context.Context, imageURL string, opts DownloadOptions, secopts security.Options) (ImageData, http.Header, error) {
+	h := make(http.Header)
+
 	// We use this for testing
 	if len(redirectAllRequestsTo) > 0 {
 		imageURL = redirectAllRequestsTo
@@ -45,16 +47,19 @@ func download(ctx context.Context, imageURL string, opts DownloadOptions, secopt
 
 	req, err := Fetcher.BuildRequest(ctx, imageURL, opts.Header, opts.CookieJar)
 	if err != nil {
-		return nil, err
+		return nil, h, err
 	}
 	defer req.Cancel()
 
 	res, err := req.FetchImage()
+	if res != nil {
+		h = res.Header.Clone()
+	}
 	if err != nil {
 		if res != nil {
 			res.Body.Close()
 		}
-		return nil, err
+		return nil, h, err
 	}
 
 	res, err = security.LimitResponseSize(res, secopts)
@@ -62,22 +67,15 @@ func download(ctx context.Context, imageURL string, opts DownloadOptions, secopt
 		defer res.Body.Close()
 	}
 	if err != nil {
-		return nil, err
+		return nil, h, err
 	}
 
 	imgdata, err := readAndCheckImage(res.Body, int(res.ContentLength), secopts)
 	if err != nil {
-		return nil, ierrors.Wrap(err, 0)
-	}
-
-	// NOTE: This will be removed in the future in favor of headers/image data separation
-	for k, v := range res.Header {
-		for _, v := range v {
-			imgdata.Headers().Add(k, v)
-		}
+		return nil, h, ierrors.Wrap(err, 0)
 	}
 
-	return imgdata, nil
+	return imgdata, h, nil
 }
 
 func RedirectAllRequestsTo(u string) {

+ 7 - 17
imagedata/factory.go

@@ -4,7 +4,6 @@ import (
 	"bytes"
 	"encoding/base64"
 	"io"
-	"net/http"
 	"os"
 
 	"github.com/imgproxy/imgproxy/v3/imagemeta"
@@ -12,22 +11,13 @@ import (
 	"github.com/imgproxy/imgproxy/v3/security"
 )
 
-// NewFromBytesWithFormat creates a new ImageData instance from the provided format,
-// http headers and byte slice.
-func NewFromBytesWithFormat(format imagetype.Type, b []byte, headers http.Header) ImageData {
-	var h http.Header
-
-	if headers == nil {
-		h = make(http.Header)
-	} else {
-		h = headers.Clone()
-	}
-
+// NewFromBytesWithFormat creates a new ImageData instance from the provided format
+// and byte slice.
+func NewFromBytesWithFormat(format imagetype.Type, b []byte) ImageData {
 	return &imageDataBytes{
-		data:    b,
-		format:  format,
-		headers: h,
-		cancel:  nil,
+		data:   b,
+		format: format,
+		cancel: nil,
 	}
 }
 
@@ -40,7 +30,7 @@ func NewFromBytes(b []byte) (ImageData, error) {
 		return nil, err
 	}
 
-	return NewFromBytesWithFormat(meta.Format(), b, nil), nil
+	return NewFromBytesWithFormat(meta.Format(), b), nil
 }
 
 // NewFromPath creates a new ImageData from an os.File

+ 11 - 23
imagedata/image_data.go

@@ -15,8 +15,9 @@ import (
 )
 
 var (
-	Watermark     ImageData
-	FallbackImage ImageData
+	Watermark            ImageData
+	FallbackImage        ImageData
+	FallbackImageHeaders http.Header // Headers for the fallback image
 )
 
 type ImageData interface {
@@ -25,17 +26,12 @@ type ImageData interface {
 	Format() imagetype.Type       // Format returns the image format from the metadata (shortcut)
 	Size() (int, error)           // Size returns the size of the image data in bytes
 	AddCancel(context.CancelFunc) // AddCancel attaches a cancel function to the image data
-
-	// This will be removed in the future
-	Headers() http.Header // Headers returns the HTTP headers of the image data, will be removed in the future
 }
 
 // imageDataBytes represents image data stored in a byte slice in memory
 type imageDataBytes struct {
-	format  imagetype.Type
-	data    []byte
-	headers http.Header
-
+	format     imagetype.Type
+	data       []byte
 	cancel     []context.CancelFunc
 	cancelOnce sync.Once
 }
@@ -66,10 +62,6 @@ func (d *imageDataBytes) Size() (int, error) {
 	return len(d.data), nil
 }
 
-func (d *imageDataBytes) Headers() http.Header {
-	return d.headers
-}
-
 func (d *imageDataBytes) AddCancel(cancel context.CancelFunc) {
 	d.cancel = append(d.cancel, cancel)
 }
@@ -113,7 +105,7 @@ func loadWatermark() error {
 		}
 
 	case len(config.WatermarkURL) > 0:
-		Watermark, err = Download(context.Background(), config.WatermarkURL, "watermark", DownloadOptions{Header: nil, CookieJar: nil}, security.DefaultOptions())
+		Watermark, _, err = Download(context.Background(), config.WatermarkURL, "watermark", DownloadOptions{Header: nil, CookieJar: nil}, security.DefaultOptions())
 		if err != nil {
 			return ierrors.Wrap(err, 0, ierrors.WithPrefix("can't download from URL"))
 		}
@@ -140,7 +132,7 @@ func loadFallbackImage() (err error) {
 		}
 
 	case len(config.FallbackImageURL) > 0:
-		FallbackImage, err = Download(context.Background(), config.FallbackImageURL, "fallback image", DownloadOptions{Header: nil, CookieJar: nil}, security.DefaultOptions())
+		FallbackImage, FallbackImageHeaders, err = Download(context.Background(), config.FallbackImageURL, "fallback image", DownloadOptions{Header: nil, CookieJar: nil}, security.DefaultOptions())
 		if err != nil {
 			return ierrors.Wrap(err, 0, ierrors.WithPrefix("can't download from URL"))
 		}
@@ -149,21 +141,17 @@ func loadFallbackImage() (err error) {
 		FallbackImage = nil
 	}
 
-	if FallbackImage != nil && err == nil && config.FallbackImageTTL > 0 {
-		FallbackImage.Headers().Set("Fallback-Image", "1")
-	}
-
 	return err
 }
 
-func Download(ctx context.Context, imageURL, desc string, opts DownloadOptions, secopts security.Options) (ImageData, error) {
-	imgdata, err := download(ctx, imageURL, opts, secopts)
+func Download(ctx context.Context, imageURL, desc string, opts DownloadOptions, secopts security.Options) (ImageData, http.Header, error) {
+	imgdata, h, err := download(ctx, imageURL, opts, secopts)
 	if err != nil {
-		return nil, ierrors.Wrap(
+		return nil, h, ierrors.Wrap(
 			err, 0,
 			ierrors.WithPrefix(fmt.Sprintf("Can't download %s", desc)),
 		)
 	}
 
-	return imgdata, nil
+	return imgdata, h, nil
 }

+ 11 - 11
imagedata/image_data_test.go

@@ -91,7 +91,7 @@ func (s *ImageDataTestSuite) SetupTest() {
 }
 
 func (s *ImageDataTestSuite) TestDownloadStatusOK() {
-	imgdata, err := Download(context.Background(), s.server.URL, "Test image", DownloadOptions{}, security.DefaultOptions())
+	imgdata, _, err := Download(context.Background(), s.server.URL, "Test image", DownloadOptions{}, security.DefaultOptions())
 
 	s.Require().NoError(err)
 	s.Require().NotNil(imgdata)
@@ -158,7 +158,7 @@ func (s *ImageDataTestSuite) TestDownloadStatusPartialContent() {
 		s.Run(tc.name, func() {
 			s.header.Set("Content-Range", tc.contentRange)
 
-			imgdata, err := Download(context.Background(), s.server.URL, "Test image", DownloadOptions{}, security.DefaultOptions())
+			imgdata, _, err := Download(context.Background(), s.server.URL, "Test image", DownloadOptions{}, security.DefaultOptions())
 
 			if tc.expectErr {
 				s.Require().Error(err)
@@ -178,7 +178,7 @@ func (s *ImageDataTestSuite) TestDownloadStatusNotFound() {
 	s.data = []byte("Not Found")
 	s.header.Set("Content-Type", "text/plain")
 
-	imgdata, err := Download(context.Background(), s.server.URL, "Test image", DownloadOptions{}, security.DefaultOptions())
+	imgdata, _, err := Download(context.Background(), s.server.URL, "Test image", DownloadOptions{}, security.DefaultOptions())
 
 	s.Require().Error(err)
 	s.Require().Equal(404, ierrors.Wrap(err, 0).StatusCode())
@@ -190,7 +190,7 @@ func (s *ImageDataTestSuite) TestDownloadStatusForbidden() {
 	s.data = []byte("Forbidden")
 	s.header.Set("Content-Type", "text/plain")
 
-	imgdata, err := Download(context.Background(), s.server.URL, "Test image", DownloadOptions{}, security.DefaultOptions())
+	imgdata, _, err := Download(context.Background(), s.server.URL, "Test image", DownloadOptions{}, security.DefaultOptions())
 
 	s.Require().Error(err)
 	s.Require().Equal(404, ierrors.Wrap(err, 0).StatusCode())
@@ -202,7 +202,7 @@ func (s *ImageDataTestSuite) TestDownloadStatusInternalServerError() {
 	s.data = []byte("Internal Server Error")
 	s.header.Set("Content-Type", "text/plain")
 
-	imgdata, err := Download(context.Background(), s.server.URL, "Test image", DownloadOptions{}, security.DefaultOptions())
+	imgdata, _, err := Download(context.Background(), s.server.URL, "Test image", DownloadOptions{}, security.DefaultOptions())
 
 	s.Require().Error(err)
 	s.Require().Equal(500, ierrors.Wrap(err, 0).StatusCode())
@@ -216,7 +216,7 @@ func (s *ImageDataTestSuite) TestDownloadUnreachable() {
 
 	serverURL := fmt.Sprintf("http://%s", l.Addr().String())
 
-	imgdata, err := Download(context.Background(), serverURL, "Test image", DownloadOptions{}, security.DefaultOptions())
+	imgdata, _, err := Download(context.Background(), serverURL, "Test image", DownloadOptions{}, security.DefaultOptions())
 
 	s.Require().Error(err)
 	s.Require().Equal(500, ierrors.Wrap(err, 0).StatusCode())
@@ -226,7 +226,7 @@ func (s *ImageDataTestSuite) TestDownloadUnreachable() {
 func (s *ImageDataTestSuite) TestDownloadInvalidImage() {
 	s.data = []byte("invalid")
 
-	imgdata, err := Download(context.Background(), s.server.URL, "Test image", DownloadOptions{}, security.DefaultOptions())
+	imgdata, _, err := Download(context.Background(), s.server.URL, "Test image", DownloadOptions{}, security.DefaultOptions())
 
 	s.Require().Error(err)
 	s.Require().Equal(422, ierrors.Wrap(err, 0).StatusCode())
@@ -236,7 +236,7 @@ func (s *ImageDataTestSuite) TestDownloadInvalidImage() {
 func (s *ImageDataTestSuite) TestDownloadSourceAddressNotAllowed() {
 	config.AllowLoopbackSourceAddresses = false
 
-	imgdata, err := Download(context.Background(), s.server.URL, "Test image", DownloadOptions{}, security.DefaultOptions())
+	imgdata, _, err := Download(context.Background(), s.server.URL, "Test image", DownloadOptions{}, security.DefaultOptions())
 
 	s.Require().Error(err)
 	s.Require().Equal(404, ierrors.Wrap(err, 0).StatusCode())
@@ -246,7 +246,7 @@ func (s *ImageDataTestSuite) TestDownloadSourceAddressNotAllowed() {
 func (s *ImageDataTestSuite) TestDownloadImageTooLarge() {
 	config.MaxSrcResolution = 1
 
-	imgdata, err := Download(context.Background(), s.server.URL, "Test image", DownloadOptions{}, security.DefaultOptions())
+	imgdata, _, err := Download(context.Background(), s.server.URL, "Test image", DownloadOptions{}, security.DefaultOptions())
 
 	s.Require().Error(err)
 	s.Require().Equal(422, ierrors.Wrap(err, 0).StatusCode())
@@ -256,7 +256,7 @@ func (s *ImageDataTestSuite) TestDownloadImageTooLarge() {
 func (s *ImageDataTestSuite) TestDownloadImageFileTooLarge() {
 	config.MaxSrcFileSize = 1
 
-	imgdata, err := Download(context.Background(), s.server.URL, "Test image", DownloadOptions{}, security.DefaultOptions())
+	imgdata, _, err := Download(context.Background(), s.server.URL, "Test image", DownloadOptions{}, security.DefaultOptions())
 
 	s.Require().Error(err)
 	s.Require().Equal(422, ierrors.Wrap(err, 0).StatusCode())
@@ -275,7 +275,7 @@ func (s *ImageDataTestSuite) TestDownloadGzip() {
 	s.data = buf.Bytes()
 	s.header.Set("Content-Encoding", "gzip")
 
-	imgdata, err := Download(context.Background(), s.server.URL, "Test image", DownloadOptions{}, security.DefaultOptions())
+	imgdata, _, err := Download(context.Background(), s.server.URL, "Test image", DownloadOptions{}, security.DefaultOptions())
 
 	s.Require().NoError(err)
 	s.Require().NotNil(imgdata)

+ 1 - 1
imagedata/read.go

@@ -49,7 +49,7 @@ func readAndCheckImage(r io.Reader, contentLength int, secopts security.Options)
 		return nil, imagefetcher.WrapError(err)
 	}
 
-	i := NewFromBytesWithFormat(meta.Format(), buf.Bytes(), nil)
+	i := NewFromBytesWithFormat(meta.Format(), buf.Bytes())
 	i.AddCancel(cancel)
 	return i, nil
 }

+ 19 - 9
processing/processing.go

@@ -4,7 +4,6 @@ import (
 	"context"
 	"errors"
 	"runtime"
-	"strconv"
 
 	log "github.com/sirupsen/logrus"
 
@@ -79,7 +78,7 @@ func ValidatePreferredFormats() error {
 	}
 
 	if len(filtered) == 0 {
-		return errors.New("No supported preferred formats specified")
+		return errors.New("no supported preferred formats specified")
 	}
 
 	config.PreferredFormats = filtered
@@ -248,7 +247,15 @@ func saveImageToFitBytes(ctx context.Context, po *options.ProcessingOptions, img
 	}
 }
 
-func ProcessImage(ctx context.Context, imgdata imagedata.ImageData, po *options.ProcessingOptions) (imagedata.ImageData, error) {
+type Result struct {
+	OutData      imagedata.ImageData
+	OriginWidth  int
+	OriginHeight int
+	ResultWidth  int
+	ResultHeight int
+}
+
+func ProcessImage(ctx context.Context, imgdata imagedata.ImageData, po *options.ProcessingOptions) (*Result, error) {
 	runtime.LockOSThread()
 	defer runtime.UnlockOSThread()
 
@@ -358,12 +365,15 @@ func ProcessImage(ctx context.Context, imgdata imagedata.ImageData, po *options.
 		outData, err = img.Save(po.Format, po.GetQuality())
 	}
 
-	if err == nil {
-		outData.Headers().Set("X-Origin-Width", strconv.Itoa(originWidth))
-		outData.Headers().Set("X-Origin-Height", strconv.Itoa(originHeight))
-		outData.Headers().Set("X-Result-Width", strconv.Itoa(img.Width()))
-		outData.Headers().Set("X-Result-Height", strconv.Itoa(img.Height()))
+	if err != nil {
+		return nil, err
 	}
 
-	return outData, err
+	return &Result{
+		OutData:      outData,
+		OriginWidth:  originWidth,
+		OriginHeight: originHeight,
+		ResultWidth:  img.Width(),
+		ResultHeight: img.Height(),
+	}, nil
 }

+ 44 - 47
processing/processing_test.go

@@ -49,14 +49,10 @@ func (s *ProcessingTestSuite) openFile(name string) imagedata.ImageData {
 	return imagedata
 }
 
-func (s *ProcessingTestSuite) checkSize(imgdata imagedata.ImageData, width, height int) {
-	img := new(vips.Image)
-	err := img.Load(imgdata, 1, 1, 1)
-	s.Require().NoError(err)
-	defer img.Clear()
-
-	s.Require().Equal(width, img.Width(), "Width mismatch")
-	s.Require().Equal(height, img.Height(), "Height mismatch")
+func (s *ProcessingTestSuite) checkSize(r *Result, width, height int) {
+	s.Require().NotNil(r)
+	s.Require().Equal(width, r.ResultWidth, "Width mismatch")
+	s.Require().Equal(height, r.ResultHeight, "Height mismatch")
 }
 
 func (s *ProcessingTestSuite) TestResizeToFit() {
@@ -88,11 +84,11 @@ func (s *ProcessingTestSuite) TestResizeToFit() {
 			po.Width = tc.width
 			po.Height = tc.height
 
-			outImgdata, err := ProcessImage(context.Background(), imgdata, po)
+			result, err := ProcessImage(context.Background(), imgdata, po)
 			s.Require().NoError(err)
-			s.Require().NotNil(outImgdata)
+			s.Require().NotNil(result)
 
-			s.checkSize(outImgdata, tc.outWidth, tc.outHeight)
+			s.checkSize(result, tc.outWidth, tc.outHeight)
 		})
 	}
 }
@@ -127,11 +123,11 @@ func (s *ProcessingTestSuite) TestResizeToFitEnlarge() {
 			po.Width = tc.width
 			po.Height = tc.height
 
-			outImgdata, err := ProcessImage(context.Background(), imgdata, po)
+			result, err := ProcessImage(context.Background(), imgdata, po)
 			s.Require().NoError(err)
-			s.Require().NotNil(outImgdata)
+			s.Require().NotNil(result)
 
-			s.checkSize(outImgdata, tc.outWidth, tc.outHeight)
+			s.checkSize(result, tc.outWidth, tc.outHeight)
 		})
 	}
 }
@@ -171,11 +167,11 @@ func (s *ProcessingTestSuite) TestResizeToFitExtend() {
 			po.Width = tc.width
 			po.Height = tc.height
 
-			outImgdata, err := ProcessImage(context.Background(), imgdata, po)
+			result, err := ProcessImage(context.Background(), imgdata, po)
 			s.Require().NoError(err)
-			s.Require().NotNil(outImgdata)
+			s.Require().NotNil(result)
 
-			s.checkSize(outImgdata, tc.outWidth, tc.outHeight)
+			s.checkSize(result, tc.outWidth, tc.outHeight)
 		})
 	}
 }
@@ -215,11 +211,11 @@ func (s *ProcessingTestSuite) TestResizeToFitExtendAR() {
 			po.Width = tc.width
 			po.Height = tc.height
 
-			outImgdata, err := ProcessImage(context.Background(), imgdata, po)
+			result, err := ProcessImage(context.Background(), imgdata, po)
 			s.Require().NoError(err)
-			s.Require().NotNil(outImgdata)
+			s.Require().NotNil(result)
 
-			s.checkSize(outImgdata, tc.outWidth, tc.outHeight)
+			s.checkSize(result, tc.outWidth, tc.outHeight)
 		})
 	}
 }
@@ -253,11 +249,11 @@ func (s *ProcessingTestSuite) TestResizeToFill() {
 			po.Width = tc.width
 			po.Height = tc.height
 
-			outImgdata, err := ProcessImage(context.Background(), imgdata, po)
+			result, err := ProcessImage(context.Background(), imgdata, po)
 			s.Require().NoError(err)
-			s.Require().NotNil(outImgdata)
+			s.Require().NotNil(result)
 
-			s.checkSize(outImgdata, tc.outWidth, tc.outHeight)
+			s.checkSize(result, tc.outWidth, tc.outHeight)
 		})
 	}
 }
@@ -292,11 +288,11 @@ func (s *ProcessingTestSuite) TestResizeToFillEnlarge() {
 			po.Width = tc.width
 			po.Height = tc.height
 
-			outImgdata, err := ProcessImage(context.Background(), imgdata, po)
+			result, err := ProcessImage(context.Background(), imgdata, po)
 			s.Require().NoError(err)
-			s.Require().NotNil(outImgdata)
+			s.Require().NotNil(result)
 
-			s.checkSize(outImgdata, tc.outWidth, tc.outHeight)
+			s.checkSize(result, tc.outWidth, tc.outHeight)
 		})
 	}
 }
@@ -338,11 +334,11 @@ func (s *ProcessingTestSuite) TestResizeToFillExtend() {
 			po.Width = tc.width
 			po.Height = tc.height
 
-			outImgdata, err := ProcessImage(context.Background(), imgdata, po)
+			result, err := ProcessImage(context.Background(), imgdata, po)
 			s.Require().NoError(err)
-			s.Require().NotNil(outImgdata)
+			s.Require().NotNil(result)
 
-			s.checkSize(outImgdata, tc.outWidth, tc.outHeight)
+			s.checkSize(result, tc.outWidth, tc.outHeight)
 		})
 	}
 }
@@ -384,11 +380,11 @@ func (s *ProcessingTestSuite) TestResizeToFillExtendAR() {
 			po.Width = tc.width
 			po.Height = tc.height
 
-			outImgdata, err := ProcessImage(context.Background(), imgdata, po)
+			result, err := ProcessImage(context.Background(), imgdata, po)
 			s.Require().NoError(err)
-			s.Require().NotNil(outImgdata)
+			s.Require().NotNil(result)
 
-			s.checkSize(outImgdata, tc.outWidth, tc.outHeight)
+			s.checkSize(result, tc.outWidth, tc.outHeight)
 		})
 	}
 }
@@ -422,11 +418,11 @@ func (s *ProcessingTestSuite) TestResizeToFillDown() {
 			po.Width = tc.width
 			po.Height = tc.height
 
-			outImgdata, err := ProcessImage(context.Background(), imgdata, po)
+			result, err := ProcessImage(context.Background(), imgdata, po)
 			s.Require().NoError(err)
-			s.Require().NotNil(outImgdata)
+			s.Require().NotNil(result)
 
-			s.checkSize(outImgdata, tc.outWidth, tc.outHeight)
+			s.checkSize(result, tc.outWidth, tc.outHeight)
 		})
 	}
 }
@@ -461,11 +457,11 @@ func (s *ProcessingTestSuite) TestResizeToFillDownEnlarge() {
 			po.Width = tc.width
 			po.Height = tc.height
 
-			outImgdata, err := ProcessImage(context.Background(), imgdata, po)
+			result, err := ProcessImage(context.Background(), imgdata, po)
 			s.Require().NoError(err)
-			s.Require().NotNil(outImgdata)
+			s.Require().NotNil(result)
 
-			s.checkSize(outImgdata, tc.outWidth, tc.outHeight)
+			s.checkSize(result, tc.outWidth, tc.outHeight)
 		})
 	}
 }
@@ -507,11 +503,11 @@ func (s *ProcessingTestSuite) TestResizeToFillDownExtend() {
 			po.Width = tc.width
 			po.Height = tc.height
 
-			outImgdata, err := ProcessImage(context.Background(), imgdata, po)
+			result, err := ProcessImage(context.Background(), imgdata, po)
 			s.Require().NoError(err)
-			s.Require().NotNil(outImgdata)
+			s.Require().NotNil(result)
 
-			s.checkSize(outImgdata, tc.outWidth, tc.outHeight)
+			s.checkSize(result, tc.outWidth, tc.outHeight)
 		})
 	}
 }
@@ -551,11 +547,11 @@ func (s *ProcessingTestSuite) TestResizeToFillDownExtendAR() {
 			po.Width = tc.width
 			po.Height = tc.height
 
-			outImgdata, err := ProcessImage(context.Background(), imgdata, po)
+			result, err := ProcessImage(context.Background(), imgdata, po)
 			s.Require().NoError(err)
-			s.Require().NotNil(outImgdata)
+			s.Require().NotNil(result)
 
-			s.checkSize(outImgdata, tc.outWidth, tc.outHeight)
+			s.checkSize(result, tc.outWidth, tc.outHeight)
 		})
 	}
 }
@@ -980,11 +976,12 @@ func (s *ProcessingTestSuite) TestResultSizeLimit() {
 			po.Rotate = tc.rotate
 			po.Padding = tc.padding
 
-			outImgdata, err := ProcessImage(context.Background(), imgdata, po)
+			result, err := ProcessImage(context.Background(), imgdata, po)
+
 			s.Require().NoError(err)
-			s.Require().NotNil(outImgdata)
+			s.Require().NotNil(result)
 
-			s.checkSize(outImgdata, tc.outWidth, tc.outHeight)
+			s.checkSize(result, tc.outWidth, tc.outHeight)
 		})
 	}
 }

+ 49 - 28
processing_handler.go

@@ -125,7 +125,31 @@ func setCanonical(rw http.ResponseWriter, originURL string) {
 	}
 }
 
-func respondWithImage(reqID string, r *http.Request, rw http.ResponseWriter, statusCode int, resultData imagedata.ImageData, po *options.ProcessingOptions, originURL string, originData imagedata.ImageData) {
+func writeOriginContentLengthDebugHeader(ctx context.Context, rw http.ResponseWriter, originData imagedata.ImageData) {
+	if !config.EnableDebugHeaders {
+		return
+	}
+
+	size, err := originData.Size()
+	if err != nil {
+		checkErr(ctx, "image_data_size", err)
+	}
+
+	rw.Header().Set(httpheaders.XOriginContentLength, strconv.Itoa(size))
+}
+
+func writeDebugHeaders(rw http.ResponseWriter, result *processing.Result) {
+	if !config.EnableDebugHeaders || result == nil {
+		return
+	}
+
+	rw.Header().Set(httpheaders.XOriginWidth, strconv.Itoa(result.OriginWidth))
+	rw.Header().Set(httpheaders.XOriginHeight, strconv.Itoa(result.OriginHeight))
+	rw.Header().Set(httpheaders.XResultWidth, strconv.Itoa(result.ResultWidth))
+	rw.Header().Set(httpheaders.XResultHeight, strconv.Itoa(result.ResultHeight))
+}
+
+func respondWithImage(reqID string, r *http.Request, rw http.ResponseWriter, statusCode int, resultData imagedata.ImageData, po *options.ProcessingOptions, originURL string, originData imagedata.ImageData, originHeaders http.Header) {
 	var contentDisposition string
 	if len(po.Filename) > 0 {
 		contentDisposition = resultData.Format().ContentDisposition(po.Filename, po.ReturnAttachment)
@@ -133,35 +157,22 @@ func respondWithImage(reqID string, r *http.Request, rw http.ResponseWriter, sta
 		contentDisposition = resultData.Format().ContentDispositionFromURL(originURL, po.ReturnAttachment)
 	}
 
-	rw.Header().Set("Content-Type", resultData.Format().Mime())
-	rw.Header().Set("Content-Disposition", contentDisposition)
+	rw.Header().Set(httpheaders.ContentType, resultData.Format().Mime())
+	rw.Header().Set(httpheaders.ContentDisposition, contentDisposition)
 
-	setCacheControl(rw, po.Expires, originData.Headers())
-	setLastModified(rw, originData.Headers())
+	setCacheControl(rw, po.Expires, originHeaders)
+	setLastModified(rw, originHeaders)
 	setVary(rw)
 	setCanonical(rw, originURL)
 
-	if config.EnableDebugHeaders {
-		originSize, err := originData.Size()
-		if err != nil {
-			checkErr(r.Context(), "image_data_size", err)
-		}
-
-		rw.Header().Set("X-Origin-Content-Length", strconv.Itoa(originSize))
-		rw.Header().Set("X-Origin-Width", resultData.Headers().Get("X-Origin-Width"))
-		rw.Header().Set("X-Origin-Height", resultData.Headers().Get("X-Origin-Height"))
-		rw.Header().Set("X-Result-Width", resultData.Headers().Get("X-Result-Width"))
-		rw.Header().Set("X-Result-Height", resultData.Headers().Get("X-Result-Height"))
-	}
-
-	rw.Header().Set("Content-Security-Policy", "script-src 'none'")
+	rw.Header().Set(httpheaders.ContentSecurityPolicy, "script-src 'none'")
 
 	resultSize, err := resultData.Size()
 	if err != nil {
 		checkErr(r.Context(), "image_data_size", err)
 	}
 
-	rw.Header().Set("Content-Length", strconv.Itoa(resultSize))
+	rw.Header().Set(httpheaders.ContentLength, strconv.Itoa(resultSize))
 	rw.WriteHeader(statusCode)
 
 	_, err = io.Copy(rw, resultData.Reader())
@@ -348,7 +359,7 @@ func handleProcessing(reqID string, rw http.ResponseWriter, r *http.Request) {
 
 	statusCode := http.StatusOK
 
-	originData, err := func() (imagedata.ImageData, error) {
+	originData, originHeaders, err := func() (imagedata.ImageData, http.Header, error) {
 		defer metrics.StartDownloadingSegment(ctx, metrics.Meta{
 			metrics.MetaSourceImageURL:    metricsMeta[metrics.MetaSourceImageURL],
 			metrics.MetaSourceImageOrigin: metricsMeta[metrics.MetaSourceImageOrigin],
@@ -412,17 +423,22 @@ func handleProcessing(reqID string, rw http.ResponseWriter, r *http.Request) {
 		}
 
 		originData = imagedata.FallbackImage
+		originHeaders = imagedata.FallbackImageHeaders.Clone()
+
+		if config.FallbackImageTTL > 0 {
+			originHeaders.Set("Fallback-Image", "1")
+		}
 	}
 
 	checkErr(ctx, "timeout", router.CheckTimeout(ctx))
 
 	if config.ETagEnabled && statusCode == http.StatusOK {
-		imgDataMatch, terr := etagHandler.SetActualImageData(originData)
+		imgDataMatch, terr := etagHandler.SetActualImageData(originData, originHeaders)
 		if terr == nil {
 			rw.Header().Set("ETag", etagHandler.GenerateActualETag())
 
 			if imgDataMatch && etagHandler.ProcessingOptionsMatch() {
-				respondWithNotModified(reqID, r, rw, po, imageURL, originData.Headers())
+				respondWithNotModified(reqID, r, rw, po, imageURL, originHeaders)
 				return
 			}
 		}
@@ -444,11 +460,13 @@ func handleProcessing(reqID string, rw http.ResponseWriter, r *http.Request) {
 
 			defer sanitized.Close()
 
-			respondWithImage(reqID, r, rw, statusCode, sanitized, po, imageURL, originData)
+			writeOriginContentLengthDebugHeader(ctx, rw, originData)
+			respondWithImage(reqID, r, rw, statusCode, sanitized, po, imageURL, originData, originHeaders)
 			return
 		}
 
-		respondWithImage(reqID, r, rw, statusCode, originData, po, imageURL, originData)
+		writeOriginContentLengthDebugHeader(ctx, rw, originData)
+		respondWithImage(reqID, r, rw, statusCode, originData, po, imageURL, originData, originHeaders)
 		return
 	}
 
@@ -467,7 +485,7 @@ func handleProcessing(reqID string, rw http.ResponseWriter, r *http.Request) {
 		))
 	}
 
-	resultData, err := func() (imagedata.ImageData, error) {
+	result, err := func() (*processing.Result, error) {
 		defer metrics.StartProcessingSegment(ctx, metrics.Meta{
 			metrics.MetaProcessingOptions: metricsMeta[metrics.MetaProcessingOptions],
 		})()
@@ -475,9 +493,12 @@ func handleProcessing(reqID string, rw http.ResponseWriter, r *http.Request) {
 	}()
 	checkErr(ctx, "processing", err)
 
-	defer resultData.Close()
+	defer result.OutData.Close()
 
 	checkErr(ctx, "timeout", router.CheckTimeout(ctx))
 
-	respondWithImage(reqID, r, rw, statusCode, resultData, po, imageURL, originData)
+	writeDebugHeaders(rw, result)
+	writeOriginContentLengthDebugHeader(ctx, rw, originData)
+
+	respondWithImage(reqID, r, rw, statusCode, result.OutData, po, imageURL, originData, originHeaders)
 }

+ 17 - 16
processing_handler_test.go

@@ -106,7 +106,7 @@ func (s *ProcessingHandlerTestSuite) readImageData(imgdata imagedata.ImageData)
 	return data
 }
 
-func (s *ProcessingHandlerTestSuite) sampleETagData(imgETag string) (string, imagedata.ImageData, string) {
+func (s *ProcessingHandlerTestSuite) sampleETagData(imgETag string) (string, imagedata.ImageData, http.Header, string) {
 	poStr := "rs:fill:4:4"
 
 	po := options.NewProcessingOptions()
@@ -115,16 +115,17 @@ func (s *ProcessingHandlerTestSuite) sampleETagData(imgETag string) (string, ima
 	po.Height = 4
 
 	imgdata := s.readTestImageData("test1.png")
+	headers := make(http.Header)
 
 	if len(imgETag) != 0 {
-		imgdata.Headers().Set(httpheaders.Etag, imgETag)
+		headers.Set(httpheaders.Etag, imgETag)
 	}
 
 	var h etag.Handler
 
 	h.SetActualProcessingOptions(po)
-	h.SetActualImageData(imgdata)
-	return poStr, imgdata, h.GenerateActualETag()
+	h.SetActualImageData(imgdata, headers)
+	return poStr, imgdata, headers, h.GenerateActualETag()
 }
 
 func (s *ProcessingHandlerTestSuite) TestRequest() {
@@ -412,12 +413,12 @@ func (s *ProcessingHandlerTestSuite) TestETagDisabled() {
 func (s *ProcessingHandlerTestSuite) TestETagReqNoIfNotModified() {
 	config.ETagEnabled = true
 
-	poStr, imgdata, etag := s.sampleETagData("loremipsumdolor")
+	poStr, _, headers, etag := s.sampleETagData("loremipsumdolor")
 
 	ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
 		s.Empty(r.Header.Get("If-None-Match"))
 
-		rw.Header().Set("ETag", imgdata.Headers().Get(httpheaders.Etag))
+		rw.Header().Set("ETag", headers.Get(httpheaders.Etag))
 		rw.WriteHeader(200)
 		rw.Write(s.readTestFile("test1.png"))
 	}))
@@ -433,7 +434,7 @@ func (s *ProcessingHandlerTestSuite) TestETagReqNoIfNotModified() {
 func (s *ProcessingHandlerTestSuite) TestETagDataNoIfNotModified() {
 	config.ETagEnabled = true
 
-	poStr, imgdata, etag := s.sampleETagData("")
+	poStr, imgdata, _, etag := s.sampleETagData("")
 
 	ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
 		s.Empty(r.Header.Get("If-None-Match"))
@@ -453,10 +454,10 @@ func (s *ProcessingHandlerTestSuite) TestETagDataNoIfNotModified() {
 func (s *ProcessingHandlerTestSuite) TestETagReqMatch() {
 	config.ETagEnabled = true
 
-	poStr, imgdata, etag := s.sampleETagData(`"loremipsumdolor"`)
+	poStr, _, headers, etag := s.sampleETagData(`"loremipsumdolor"`)
 
 	ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
-		s.Equal(imgdata.Headers().Get(httpheaders.Etag), r.Header.Get(httpheaders.IfNoneMatch))
+		s.Equal(headers.Get(httpheaders.Etag), r.Header.Get(httpheaders.IfNoneMatch))
 
 		rw.WriteHeader(304)
 	}))
@@ -475,7 +476,7 @@ func (s *ProcessingHandlerTestSuite) TestETagReqMatch() {
 func (s *ProcessingHandlerTestSuite) TestETagDataMatch() {
 	config.ETagEnabled = true
 
-	poStr, imgdata, etag := s.sampleETagData("")
+	poStr, imgdata, _, etag := s.sampleETagData("")
 
 	ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
 		s.Empty(r.Header.Get("If-None-Match"))
@@ -498,13 +499,13 @@ func (s *ProcessingHandlerTestSuite) TestETagDataMatch() {
 func (s *ProcessingHandlerTestSuite) TestETagReqNotMatch() {
 	config.ETagEnabled = true
 
-	poStr, imgdata, actualETag := s.sampleETagData(`"loremipsumdolor"`)
-	_, _, expectedETag := s.sampleETagData(`"loremipsum"`)
+	poStr, imgdata, headers, actualETag := s.sampleETagData(`"loremipsumdolor"`)
+	_, _, _, expectedETag := s.sampleETagData(`"loremipsum"`)
 
 	ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
 		s.Equal(`"loremipsum"`, r.Header.Get("If-None-Match"))
 
-		rw.Header().Set("ETag", imgdata.Headers().Get(httpheaders.Etag))
+		rw.Header().Set("ETag", headers.Get(httpheaders.Etag))
 		rw.WriteHeader(200)
 		rw.Write(s.readImageData(imgdata))
 	}))
@@ -523,7 +524,7 @@ func (s *ProcessingHandlerTestSuite) TestETagReqNotMatch() {
 func (s *ProcessingHandlerTestSuite) TestETagDataNotMatch() {
 	config.ETagEnabled = true
 
-	poStr, imgdata, actualETag := s.sampleETagData("")
+	poStr, imgdata, _, actualETag := s.sampleETagData("")
 	// Change the data hash
 	expectedETag := actualETag[:strings.IndexByte(actualETag, '/')] + "/Dasdbefj"
 
@@ -548,14 +549,14 @@ func (s *ProcessingHandlerTestSuite) TestETagDataNotMatch() {
 func (s *ProcessingHandlerTestSuite) TestETagProcessingOptionsNotMatch() {
 	config.ETagEnabled = true
 
-	poStr, imgdata, actualETag := s.sampleETagData("")
+	poStr, imgdata, headers, actualETag := s.sampleETagData("")
 	// Change the processing options hash
 	expectedETag := "abcdefj" + actualETag[strings.IndexByte(actualETag, '/'):]
 
 	ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
 		s.Empty(r.Header.Get("If-None-Match"))
 
-		rw.Header().Set("ETag", imgdata.Headers().Get(httpheaders.Etag))
+		rw.Header().Set("ETag", headers.Get(httpheaders.Etag))
 		rw.WriteHeader(200)
 		rw.Write(s.readImageData(imgdata))
 	}))

+ 0 - 1
svg/svg.go

@@ -48,7 +48,6 @@ func Sanitize(data imagedata.ImageData) (imagedata.ImageData, error) {
 			newData := imagedata.NewFromBytesWithFormat(
 				imagetype.SVG,
 				buf.Bytes(),
-				data.Headers(),
 			)
 			newData.AddCancel(cancel)
 

+ 0 - 4
svg/svg_test.go

@@ -8,7 +8,6 @@ import (
 	"github.com/stretchr/testify/suite"
 
 	"github.com/imgproxy/imgproxy/v3/config"
-	"github.com/imgproxy/imgproxy/v3/httpheaders"
 	"github.com/imgproxy/imgproxy/v3/imagedata"
 	"github.com/imgproxy/imgproxy/v3/testutil"
 )
@@ -32,8 +31,6 @@ func (s *SvgTestSuite) readTestFile(name string) imagedata.ImageData {
 	s.Require().NoError(err)
 
 	d, err := imagedata.NewFromBytes(data)
-	d.Headers().Set(httpheaders.ContentType, "image/svg+xml")
-	d.Headers().Set(httpheaders.CacheControl, "public, max-age=12345")
 	s.Require().NoError(err)
 
 	return d
@@ -46,7 +43,6 @@ func (s *SvgTestSuite) TestSanitize() {
 
 	s.Require().NoError(err)
 	s.Require().True(testutil.ReadersEqual(s.T(), expected.Reader(), actual.Reader()))
-	s.Require().Equal(origin.Headers(), actual.Headers())
 }
 
 func TestSvg(t *testing.T) {

+ 1 - 1
vips/vips.go

@@ -470,7 +470,7 @@ func (img *Image) Save(imgtype imagetype.Type, quality int) (imagedata.ImageData
 
 	b := ptrToBytes(ptr, int(imgsize))
 
-	i := imagedata.NewFromBytesWithFormat(imgtype, b, nil)
+	i := imagedata.NewFromBytesWithFormat(imgtype, b)
 	i.AddCancel(cancel)
 
 	return i, nil