Pārlūkot izejas kodu

Another attempt to DRY

Viktor Sokolov 1 mēnesi atpakaļ
vecāks
revīzija
029ab1eec9

+ 4 - 4
config.go

@@ -4,7 +4,7 @@ import (
 	"github.com/imgproxy/imgproxy/v3/auximageprovider"
 	"github.com/imgproxy/imgproxy/v3/auximageprovider"
 	"github.com/imgproxy/imgproxy/v3/ensure"
 	"github.com/imgproxy/imgproxy/v3/ensure"
 	"github.com/imgproxy/imgproxy/v3/fetcher"
 	"github.com/imgproxy/imgproxy/v3/fetcher"
-	processinghandler "github.com/imgproxy/imgproxy/v3/handlers/processing"
+	"github.com/imgproxy/imgproxy/v3/handlers"
 	"github.com/imgproxy/imgproxy/v3/handlers/stream"
 	"github.com/imgproxy/imgproxy/v3/handlers/stream"
 	"github.com/imgproxy/imgproxy/v3/headerwriter"
 	"github.com/imgproxy/imgproxy/v3/headerwriter"
 	"github.com/imgproxy/imgproxy/v3/semaphores"
 	"github.com/imgproxy/imgproxy/v3/semaphores"
@@ -20,7 +20,7 @@ type Config struct {
 	WatermarkImage    auximageprovider.StaticConfig
 	WatermarkImage    auximageprovider.StaticConfig
 	Transport         transport.Config
 	Transport         transport.Config
 	Fetcher           fetcher.Config
 	Fetcher           fetcher.Config
-	ProcessingHandler processinghandler.Config
+	ProcessingHandler handlers.Config
 	StreamHandler     stream.Config
 	StreamHandler     stream.Config
 	Server            server.Config
 	Server            server.Config
 }
 }
@@ -34,7 +34,7 @@ func NewDefaultConfig() Config {
 		WatermarkImage:    auximageprovider.NewDefaultStaticConfig(),
 		WatermarkImage:    auximageprovider.NewDefaultStaticConfig(),
 		Transport:         transport.NewDefaultConfig(),
 		Transport:         transport.NewDefaultConfig(),
 		Fetcher:           fetcher.NewDefaultConfig(),
 		Fetcher:           fetcher.NewDefaultConfig(),
-		ProcessingHandler: processinghandler.NewDefaultConfig(),
+		ProcessingHandler: handlers.NewDefaultConfig(),
 		StreamHandler:     stream.NewDefaultConfig(),
 		StreamHandler:     stream.NewDefaultConfig(),
 		Server:            server.NewDefaultConfig(),
 		Server:            server.NewDefaultConfig(),
 	}
 	}
@@ -74,7 +74,7 @@ func LoadConfigFromEnv(c *Config) (*Config, error) {
 		return nil, err
 		return nil, err
 	}
 	}
 
 
-	if _, err = processinghandler.LoadConfigFromEnv(&c.ProcessingHandler); err != nil {
+	if _, err = handlers.LoadConfigFromEnv(&c.ProcessingHandler); err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
 
 

+ 1 - 1
handlers/processing/config.go → handlers/config.go

@@ -1,4 +1,4 @@
-package processing
+package handlers
 
 
 import (
 import (
 	"errors"
 	"errors"

+ 24 - 0
handlers/context.go

@@ -0,0 +1,24 @@
+package handlers
+
+import (
+	"github.com/imgproxy/imgproxy/v3/auximageprovider"
+	"github.com/imgproxy/imgproxy/v3/fetcher"
+	"github.com/imgproxy/imgproxy/v3/headerwriter"
+	"github.com/imgproxy/imgproxy/v3/imagedata"
+	"github.com/imgproxy/imgproxy/v3/semaphores"
+)
+
+// Context defines the input interface handler needs to operate.
+// In a nutshell, this interface strips ImgProxy definition from implementation.
+// All the dependent components could share the same global interface.
+//
+// It might as well be implemented on the Handler struct itself, no matter.
+// However, in this case, we'we got to implement it on every Handler struct.
+type Context interface {
+	HeaderWriter() *headerwriter.Writer
+	Fetcher() *fetcher.Fetcher
+	Semaphores() *semaphores.Semaphores
+	FallbackImage() auximageprovider.Provider
+	WatermarkImage() auximageprovider.Provider
+	ImageDataFactory() *imagedata.Factory
+}

+ 18 - 18
handlers/processing/errors.go → handlers/errors.go

@@ -1,4 +1,4 @@
-package processing
+package handlers
 
 
 import (
 import (
 	"fmt"
 	"fmt"
@@ -10,15 +10,15 @@ import (
 
 
 // Monitoring error categories
 // Monitoring error categories
 const (
 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
+	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 (
 type (
@@ -26,7 +26,7 @@ type (
 	InvalidURLError    string
 	InvalidURLError    string
 )
 )
 
 
-func newResponseWriteError(cause error) *ierrors.Error {
+func NewResponseWriteError(cause error) *ierrors.Error {
 	return ierrors.Wrap(
 	return ierrors.Wrap(
 		ResponseWriteError{cause},
 		ResponseWriteError{cause},
 		1,
 		1,
@@ -42,7 +42,7 @@ func (e ResponseWriteError) Unwrap() error {
 	return e.error
 	return e.error
 }
 }
 
 
-func newInvalidURLErrorf(status int, format string, args ...interface{}) error {
+func NewInvalidURLErrorf(status int, format string, args ...interface{}) error {
 	return ierrors.Wrap(
 	return ierrors.Wrap(
 		InvalidURLError(fmt.Sprintf(format, args...)),
 		InvalidURLError(fmt.Sprintf(format, args...)),
 		1,
 		1,
@@ -55,17 +55,17 @@ func newInvalidURLErrorf(status int, format string, args ...interface{}) error {
 func (e InvalidURLError) Error() string { return string(e) }
 func (e InvalidURLError) Error() string { return string(e) }
 
 
 // newCantSaveError creates "resulting image not supported" error
 // newCantSaveError creates "resulting image not supported" error
-func newCantSaveError(format imagetype.Type) error {
-	return ierrors.Wrap(newInvalidURLErrorf(
+func NewCantSaveError(format imagetype.Type) error {
+	return ierrors.Wrap(NewInvalidURLErrorf(
 		http.StatusUnprocessableEntity,
 		http.StatusUnprocessableEntity,
 		"Resulting image format is not supported: %s", format,
 		"Resulting image format is not supported: %s", format,
-	), 1, ierrors.WithCategory(categoryPathParsing))
+	), 1, ierrors.WithCategory(CategoryPathParsing))
 }
 }
 
 
 // newCantLoadError creates "source image not supported" error
 // newCantLoadError creates "source image not supported" error
-func newCantLoadError(format imagetype.Type) error {
-	return ierrors.Wrap(newInvalidURLErrorf(
+func NewCantLoadError(format imagetype.Type) error {
+	return ierrors.Wrap(NewInvalidURLErrorf(
 		http.StatusUnprocessableEntity,
 		http.StatusUnprocessableEntity,
 		"Source image format is not supported: %s", format,
 		"Source image format is not supported: %s", format,
-	), 1, ierrors.WithCategory(categoryProcessing))
+	), 1, ierrors.WithCategory(CategoryProcessing))
 }
 }

+ 3 - 3
handlers/processing/path.go → handlers/path.go

@@ -1,4 +1,4 @@
-package processing
+package handlers
 
 
 import (
 import (
 	"fmt"
 	"fmt"
@@ -30,8 +30,8 @@ func splitPathSignature(r *http.Request, config *Config) (string, string, error)
 	signature, path, _ := strings.Cut(uri, "/")
 	signature, path, _ := strings.Cut(uri, "/")
 	if len(signature) == 0 || len(path) == 0 {
 	if len(signature) == 0 || len(path) == 0 {
 		return "", "", ierrors.Wrap(
 		return "", "", ierrors.Wrap(
-			newInvalidURLErrorf(http.StatusNotFound, "Invalid path: %s", path), 0,
-			ierrors.WithCategory(categoryPathParsing),
+			NewInvalidURLErrorf(http.StatusNotFound, "Invalid path: %s", path), 0,
+			ierrors.WithCategory(CategoryPathParsing),
 		)
 		)
 	}
 	}
 
 

+ 2 - 2
handlers/processing/path_test.go → handlers/path_test.go

@@ -1,4 +1,4 @@
-package processing
+package handlers
 
 
 import (
 import (
 	"net/http"
 	"net/http"
@@ -96,7 +96,7 @@ func (s *PathTestSuite) TestParsePath() {
 
 
 				s.Require().Error(err)
 				s.Require().Error(err)
 				s.Require().ErrorAs(err, &ierr)
 				s.Require().ErrorAs(err, &ierr)
-				s.Require().Equal(categoryPathParsing, ierr.Category())
+				s.Require().Equal(CategoryPathParsing, ierr.Category())
 
 
 				return
 				return
 			}
 			}

+ 29 - 85
handlers/processing/handler.go

@@ -3,54 +3,40 @@ package processing
 import (
 import (
 	"context"
 	"context"
 	"net/http"
 	"net/http"
-	"net/url"
 
 
-	"github.com/imgproxy/imgproxy/v3/auximageprovider"
-	"github.com/imgproxy/imgproxy/v3/errorreport"
+	"github.com/imgproxy/imgproxy/v3/handlers"
 	"github.com/imgproxy/imgproxy/v3/handlers/stream"
 	"github.com/imgproxy/imgproxy/v3/handlers/stream"
-	"github.com/imgproxy/imgproxy/v3/headerwriter"
-	"github.com/imgproxy/imgproxy/v3/ierrors"
-	"github.com/imgproxy/imgproxy/v3/imagedata"
 	"github.com/imgproxy/imgproxy/v3/monitoring"
 	"github.com/imgproxy/imgproxy/v3/monitoring"
 	"github.com/imgproxy/imgproxy/v3/monitoring/stats"
 	"github.com/imgproxy/imgproxy/v3/monitoring/stats"
 	"github.com/imgproxy/imgproxy/v3/options"
 	"github.com/imgproxy/imgproxy/v3/options"
-	"github.com/imgproxy/imgproxy/v3/security"
-	"github.com/imgproxy/imgproxy/v3/semaphores"
 )
 )
 
 
 // Handler handles image processing requests
 // Handler handles image processing requests
 type Handler struct {
 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
-	watermarkImage auximageprovider.Provider
-	idf            *imagedata.Factory
+	hCtx   handlers.Context // Input context interface
+	stream *stream.Handler  // Stream handler for raw image streaming
+	config *handlers.Config // Handler configuration
+}
+
+type request struct {
+	*handlers.Request
+	Options *options.ProcessingOptions // Processing options extracted from URL
 }
 }
 
 
 // New creates new handler object
 // New creates new handler object
 func New(
 func New(
+	context handlers.Context,
 	stream *stream.Handler,
 	stream *stream.Handler,
-	hw *headerwriter.Writer,
-	semaphores *semaphores.Semaphores,
-	fi auximageprovider.Provider,
-	wi auximageprovider.Provider,
-	idf *imagedata.Factory,
-	config *Config,
+	config *handlers.Config,
 ) (*Handler, error) {
 ) (*Handler, error) {
 	if err := config.Validate(); err != nil {
 	if err := config.Validate(); err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
 
 
 	return &Handler{
 	return &Handler{
-		hw:             hw,
-		config:         config,
-		stream:         stream,
-		semaphores:     semaphores,
-		fallbackImage:  fi,
-		watermarkImage: wi,
-		idf:            idf,
+		hCtx:   context,
+		config: config,
+		stream: stream,
 	}, nil
 	}, nil
 }
 }
 
 
@@ -66,57 +52,29 @@ func (h *Handler) Execute(
 
 
 	ctx := imageRequest.Context()
 	ctx := imageRequest.Context()
 
 
-	// Verify URL signature and extract image url and processing options
-	imageURL, po, mm, err := h.newRequest(ctx, imageRequest)
+	r, po, err := handlers.NewRequest(h.hCtx, h, imageRequest, h.config, reqID, rw)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
 
 
 	// if processing options indicate raw image streaming, stream it and return
 	// if processing options indicate raw image streaming, stream it and return
 	if po.Raw {
 	if po.Raw {
-		return h.stream.Execute(ctx, imageRequest, imageURL, reqID, po, rw)
+		return h.stream.Execute(ctx, imageRequest, r.ImageURL, reqID, po, rw)
 	}
 	}
 
 
 	req := &request{
 	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(),
-		idf:            h.idf,
+		Request: r,
+		Options: po,
 	}
 	}
 
 
-	return req.execute(ctx)
+	return execute(ctx, req)
 }
 }
 
 
-// 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))
-	}
+func (h *Handler) ParsePath(path string, headers http.Header) (*options.ProcessingOptions, string, error) {
+	return options.ParsePath(path, headers)
+}
 
 
-	// get image origin and create monitoring meta object
+func (h *Handler) CreateMeta(ctx context.Context, imageURL string, po *options.ProcessingOptions) monitoring.Meta {
 	imageOrigin := imageOrigin(imageURL)
 	imageOrigin := imageOrigin(imageURL)
 
 
 	mm := monitoring.Meta{
 	mm := monitoring.Meta{
@@ -125,27 +83,13 @@ func (h *Handler) newRequest(
 		monitoring.MetaProcessingOptions: po.Diff().Flatten(),
 		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)
 	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
-	}
+	// NOTE: errorreport needs to be patched (just not in the context of this PR)
+	// set error reporting and monitoring context
+	// errorreport.SetMetadata(ctx, "Source Image URL", imageURL)
+	// errorreport.SetMetadata(ctx, "Source Image Origin", imageOrigin)
+	// errorreport.SetMetadata(ctx, "Processing Options", po)
 
 
-	return ""
+	return mm
 }
 }

+ 0 - 1
handlers/processing/handler_test.go

@@ -1 +0,0 @@
-package processing

+ 32 - 49
handlers/processing/request.go

@@ -4,55 +4,29 @@ import (
 	"context"
 	"context"
 	"errors"
 	"errors"
 	"net/http"
 	"net/http"
+	"net/url"
 
 
 	"github.com/imgproxy/imgproxy/v3/fetcher"
 	"github.com/imgproxy/imgproxy/v3/fetcher"
-	"github.com/imgproxy/imgproxy/v3/headerwriter"
+	"github.com/imgproxy/imgproxy/v3/handlers"
 	"github.com/imgproxy/imgproxy/v3/ierrors"
 	"github.com/imgproxy/imgproxy/v3/ierrors"
-	"github.com/imgproxy/imgproxy/v3/imagedata"
 	"github.com/imgproxy/imgproxy/v3/imagetype"
 	"github.com/imgproxy/imgproxy/v3/imagetype"
-	"github.com/imgproxy/imgproxy/v3/monitoring"
 	"github.com/imgproxy/imgproxy/v3/monitoring/stats"
 	"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/server"
 	"github.com/imgproxy/imgproxy/v3/vips"
 	"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
-	idf            *imagedata.Factory
-}
-
-// execute handles the actual processing logic
-func (r *request) execute(ctx context.Context) error {
+func execute(ctx context.Context, r *request) error {
 	// Check if we can save the resulting image
 	// Check if we can save the resulting image
-	canSave := vips.SupportsSave(r.po.Format) ||
-		r.po.Format == imagetype.Unknown ||
-		r.po.Format == imagetype.SVG
+	canSave := vips.SupportsSave(r.Options.Format) ||
+		r.Options.Format == imagetype.Unknown ||
+		r.Options.Format == imagetype.SVG
 
 
 	if !canSave {
 	if !canSave {
-		return newCantSaveError(r.po.Format)
-	}
-
-	// Acquire queue semaphore (if enabled)
-	releaseQueueSem, err := r.semaphores.AcquireQueue()
-	if err != nil {
-		return err
+		return handlers.NewCantSaveError(r.Options.Format)
 	}
 	}
-	defer releaseQueueSem()
 
 
 	// Acquire processing semaphore
 	// Acquire processing semaphore
-	releaseProcessingSem, err := r.acquireProcessingSem(ctx)
+	releaseProcessingSem, err := r.AcquireProcessingSem(ctx)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
@@ -66,37 +40,37 @@ func (r *request) execute(ctx context.Context) error {
 	statusCode := http.StatusOK
 	statusCode := http.StatusOK
 
 
 	// Request headers
 	// Request headers
-	imgRequestHeaders := r.makeImageRequestHeaders()
+	imgRequestHeaders := r.MakeImageRequestHeaders()
 
 
 	// create download options
 	// create download options
-	do := r.makeDownloadOptions(ctx, imgRequestHeaders)
+	do := r.MakeDownloadOptions(imgRequestHeaders, r.Options.SecurityOptions)
 
 
 	// Fetch image actual
 	// Fetch image actual
-	originData, originHeaders, err := r.fetchImage(ctx, do)
+	originData, originHeaders, err := r.FetchImage(ctx, do)
 	if err == nil {
 	if err == nil {
 		defer originData.Close() // if any originData has been opened, we need to close it
 		defer originData.Close() // if any originData has been opened, we need to close it
 	}
 	}
 
 
 	// Check that image detection didn't take too long
 	// Check that image detection didn't take too long
 	if terr := server.CheckTimeout(ctx); terr != nil {
 	if terr := server.CheckTimeout(ctx); terr != nil {
-		return ierrors.Wrap(terr, 0, ierrors.WithCategory(categoryTimeout))
+		return ierrors.Wrap(terr, 0, ierrors.WithCategory(handlers.CategoryTimeout))
 	}
 	}
 
 
 	// Respond with NotModified if image was not modified
 	// Respond with NotModified if image was not modified
 	var nmErr fetcher.NotModifiedError
 	var nmErr fetcher.NotModifiedError
 
 
 	if errors.As(err, &nmErr) {
 	if errors.As(err, &nmErr) {
-		r.hwr.SetOriginHeaders(nmErr.Headers())
+		r.HeaderWriter.SetOriginHeaders(nmErr.Headers())
 
 
-		return r.respondWithNotModified()
+		return respondWithNotModified(r)
 	}
 	}
 
 
 	// Prepare to write image response headers
 	// Prepare to write image response headers
-	r.hwr.SetOriginHeaders(originHeaders)
+	r.HeaderWriter.SetOriginHeaders(originHeaders)
 
 
 	// If error is not related to NotModified, respond with fallback image and replace image data
 	// If error is not related to NotModified, respond with fallback image and replace image data
 	if err != nil {
 	if err != nil {
-		originData, statusCode, err = r.handleDownloadError(ctx, err)
+		originData, statusCode, err = handleDownloadError(ctx, r, err)
 		if err != nil {
 		if err != nil {
 			return err
 			return err
 		}
 		}
@@ -104,11 +78,11 @@ func (r *request) execute(ctx context.Context) error {
 
 
 	// Check if image supports load from origin format
 	// Check if image supports load from origin format
 	if !vips.SupportsLoad(originData.Format()) {
 	if !vips.SupportsLoad(originData.Format()) {
-		return newCantLoadError(originData.Format())
+		return handlers.NewCantLoadError(originData.Format())
 	}
 	}
 
 
 	// Actually process the image
 	// Actually process the image
-	result, err := r.processImage(ctx, originData)
+	result, err := processImage(ctx, r, originData)
 
 
 	// Let's close resulting image data only if it differs from the source image data
 	// Let's close resulting image data only if it differs from the source image data
 	if result != nil && result.OutData != nil && result.OutData != originData {
 	if result != nil && result.OutData != nil && result.OutData != originData {
@@ -117,26 +91,35 @@ func (r *request) execute(ctx context.Context) error {
 
 
 	// First, check if the processing error wasn't caused by an image data error
 	// First, check if the processing error wasn't caused by an image data error
 	if derr := originData.Error(); derr != nil {
 	if derr := originData.Error(); derr != nil {
-		return ierrors.Wrap(derr, 0, ierrors.WithCategory(categoryDownload))
+		return ierrors.Wrap(derr, 0, ierrors.WithCategory(handlers.CategoryDownload))
 	}
 	}
 
 
 	// If it wasn't, than it was a processing error
 	// If it wasn't, than it was a processing error
 	if err != nil {
 	if err != nil {
-		return ierrors.Wrap(err, 0, ierrors.WithCategory(categoryProcessing))
+		return ierrors.Wrap(err, 0, ierrors.WithCategory(handlers.CategoryProcessing))
 	}
 	}
 
 
 	// Write debug headers. It seems unlogical to move they to headerwriter since they're
 	// Write debug headers. It seems unlogical to move they to headerwriter since they're
 	// not used anywhere else.
 	// not used anywhere else.
-	err = r.writeDebugHeaders(result, originData)
+	err = writeDebugHeaders(r, result, originData)
 	if err != nil {
 	if err != nil {
-		return ierrors.Wrap(err, 0, ierrors.WithCategory(categoryImageDataSize))
+		return ierrors.Wrap(err, 0, ierrors.WithCategory(handlers.CategoryImageDataSize))
 	}
 	}
 
 
 	// Responde with actual image
 	// Responde with actual image
-	err = r.respondWithImage(statusCode, result.OutData)
+	err = respondWithImage(r, statusCode, result.OutData)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
 
 
 	return nil
 	return 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 ""
+}

+ 82 - 145
handlers/processing/request_methods.go

@@ -6,114 +6,48 @@ import (
 	"net/http"
 	"net/http"
 	"strconv"
 	"strconv"
 
 
-	"github.com/imgproxy/imgproxy/v3/cookies"
 	"github.com/imgproxy/imgproxy/v3/errorreport"
 	"github.com/imgproxy/imgproxy/v3/errorreport"
+	"github.com/imgproxy/imgproxy/v3/handlers"
 	"github.com/imgproxy/imgproxy/v3/httpheaders"
 	"github.com/imgproxy/imgproxy/v3/httpheaders"
 	"github.com/imgproxy/imgproxy/v3/ierrors"
 	"github.com/imgproxy/imgproxy/v3/ierrors"
 	"github.com/imgproxy/imgproxy/v3/imagedata"
 	"github.com/imgproxy/imgproxy/v3/imagedata"
 	"github.com/imgproxy/imgproxy/v3/monitoring"
 	"github.com/imgproxy/imgproxy/v3/monitoring"
-	"github.com/imgproxy/imgproxy/v3/options"
 	"github.com/imgproxy/imgproxy/v3/processing"
 	"github.com/imgproxy/imgproxy/v3/processing"
 	"github.com/imgproxy/imgproxy/v3/server"
 	"github.com/imgproxy/imgproxy/v3/server"
 	log "github.com/sirupsen/logrus"
 	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
 // handleDownloadError replaces the image data with fallback image if needed
-func (r *request) handleDownloadError(
+func handleDownloadError(
 	ctx context.Context,
 	ctx context.Context,
+	r *request,
 	originalErr error,
 	originalErr error,
 ) (imagedata.ImageData, int, error) {
 ) (imagedata.ImageData, int, error) {
-	err := r.wrapDownloadingErr(originalErr)
+	err := r.WrapDownloadingErr(originalErr)
 
 
 	// If there is no fallback image configured, just return the error
 	// If there is no fallback image configured, just return the error
-	data, headers := r.getFallbackImage(ctx, r.po)
+	data, headers := getFallbackImage(ctx, r)
 	if data == nil {
 	if data == nil {
 		return nil, 0, err
 		return nil, 0, err
 	}
 	}
 
 
 	// Just send error
 	// Just send error
-	monitoring.SendError(ctx, categoryDownload, err)
+	monitoring.SendError(ctx, handlers.CategoryDownload, err)
 
 
 	// We didn't return, so we have to report error
 	// We didn't return, so we have to report error
 	if err.ShouldReport() {
 	if err.ShouldReport() {
-		errorreport.Report(err, r.imageRequest)
+		errorreport.Report(err, r.Req)
 	}
 	}
 
 
 	log.
 	log.
-		WithField("request_id", r.reqID).
-		Warningf("Could not load image %s. Using fallback image. %s", r.imageURL, err.Error())
+		WithField("request_id", r.ID).
+		Warningf("Could not load image %s. Using fallback image. %s", r.ImageURL, err.Error())
 
 
 	var statusCode int
 	var statusCode int
 
 
 	// Set status code if needed
 	// Set status code if needed
-	if r.config.FallbackImageHTTPCode > 0 {
-		statusCode = r.config.FallbackImageHTTPCode
+	if r.Config.FallbackImageHTTPCode > 0 {
+		statusCode = r.Config.FallbackImageHTTPCode
 	} else {
 	} else {
 		statusCode = err.StatusCode()
 		statusCode = err.StatusCode()
 	}
 	}
@@ -122,27 +56,27 @@ func (r *request) handleDownloadError(
 	headers.Del(httpheaders.Expires)
 	headers.Del(httpheaders.Expires)
 	headers.Del(httpheaders.LastModified)
 	headers.Del(httpheaders.LastModified)
 
 
-	r.hwr.SetOriginHeaders(headers)
-	r.hwr.SetIsFallbackImage()
+	r.HeaderWriter.SetOriginHeaders(headers)
+	r.HeaderWriter.SetIsFallbackImage()
 
 
 	return data, statusCode, nil
 	return data, statusCode, nil
 }
 }
 
 
 // getFallbackImage returns fallback image if any
 // getFallbackImage returns fallback image if any
-func (r *request) getFallbackImage(
+func getFallbackImage(
 	ctx context.Context,
 	ctx context.Context,
-	po *options.ProcessingOptions,
+	r *request,
 ) (imagedata.ImageData, http.Header) {
 ) (imagedata.ImageData, http.Header) {
-	if r.handler.fallbackImage == nil {
+	if r.Context.FallbackImage() == nil {
 		return nil, nil
 		return nil, nil
 	}
 	}
 
 
-	data, h, err := r.handler.fallbackImage.Get(ctx, po)
+	data, h, err := r.Context.FallbackImage().Get(ctx, r.Options)
 	if err != nil {
 	if err != nil {
 		log.Warning(err.Error())
 		log.Warning(err.Error())
 
 
-		if ierr := r.wrapDownloadingErr(err); ierr.ShouldReport() {
-			errorreport.Report(ierr, r.imageRequest)
+		if ierr := r.WrapDownloadingErr(err); ierr.ShouldReport() {
+			errorreport.Report(ierr, r.Req)
 		}
 		}
 
 
 		return nil, nil
 		return nil, nil
@@ -152,127 +86,130 @@ func (r *request) getFallbackImage(
 }
 }
 
 
 // processImage calls actual image processing
 // 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.idf)
+func processImage(
+	ctx context.Context,
+	r *request,
+	originData imagedata.ImageData,
+) (*processing.Result, error) {
+	defer monitoring.StartProcessingSegment(
+		ctx,
+		r.MonitoringMeta.Filter(monitoring.MetaProcessingOptions),
+	)()
+	return processing.ProcessImage(ctx, originData, r.Options, r.Context.WatermarkImage(), r.Context.ImageDataFactory())
 }
 }
 
 
 // writeDebugHeaders writes debug headers (X-Origin-*, X-Result-*) to the response
 // 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 {
+func writeDebugHeaders(
+	r *request,
+	result *processing.Result,
+	originData imagedata.ImageData,
+) error {
+	if !r.Config.EnableDebugHeaders {
 		return nil
 		return nil
 	}
 	}
 
 
 	if result != 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))
+		r.ResponseWriter.Header().Set(httpheaders.XOriginWidth, strconv.Itoa(result.OriginWidth))
+		r.ResponseWriter.Header().Set(httpheaders.XOriginHeight, strconv.Itoa(result.OriginHeight))
+		r.ResponseWriter.Header().Set(httpheaders.XResultWidth, strconv.Itoa(result.ResultWidth))
+		r.ResponseWriter.Header().Set(httpheaders.XResultHeight, strconv.Itoa(result.ResultHeight))
 	}
 	}
 
 
 	// Try to read origin image size
 	// Try to read origin image size
 	size, err := originData.Size()
 	size, err := originData.Size()
 	if err != nil {
 	if err != nil {
-		return ierrors.Wrap(err, 0, ierrors.WithCategory(categoryImageDataSize))
+		return ierrors.Wrap(err, 0, ierrors.WithCategory(handlers.CategoryImageDataSize))
 	}
 	}
 
 
-	r.rw.Header().Set(httpheaders.XOriginContentLength, strconv.Itoa(size))
+	r.ResponseWriter.Header().Set(httpheaders.XOriginContentLength, strconv.Itoa(size))
 
 
 	return nil
 	return nil
 }
 }
 
 
 // respondWithNotModified writes not-modified response
 // respondWithNotModified writes not-modified response
-func (r *request) respondWithNotModified() error {
-	r.hwr.SetExpires(r.po.Expires)
-	r.hwr.SetVary()
+func respondWithNotModified(r *request) error {
+	r.HeaderWriter.SetExpires(r.Options.Expires)
+	r.HeaderWriter.SetVary()
 
 
-	if r.config.LastModifiedEnabled {
-		r.hwr.Passthrough(httpheaders.LastModified)
+	if r.Config.LastModifiedEnabled {
+		r.HeaderWriter.Passthrough(httpheaders.LastModified)
 	}
 	}
 
 
-	if r.config.ETagEnabled {
-		r.hwr.Passthrough(httpheaders.Etag)
+	if r.Config.ETagEnabled {
+		r.HeaderWriter.Passthrough(httpheaders.Etag)
 	}
 	}
 
 
-	r.hwr.Write(r.rw)
+	r.HeaderWriter.Write(r.ResponseWriter)
 
 
-	r.rw.WriteHeader(http.StatusNotModified)
+	r.ResponseWriter.WriteHeader(http.StatusNotModified)
 
 
 	server.LogResponse(
 	server.LogResponse(
-		r.reqID, r.imageRequest, http.StatusNotModified, nil,
+		r.ID, r.Req, http.StatusNotModified, nil,
 		log.Fields{
 		log.Fields{
-			"image_url":          r.imageURL,
-			"processing_options": r.po,
+			"image_url":          r.ImageURL,
+			"processing_options": r.Options,
 		},
 		},
 	)
 	)
 
 
 	return nil
 	return nil
 }
 }
 
 
-func (r *request) respondWithImage(statusCode int, resultData imagedata.ImageData) error {
+func respondWithImage(r *request, statusCode int, resultData imagedata.ImageData) error {
 	// We read the size of the image data here, so we can set Content-Length header.
 	// 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
 	// This indireclty ensures that the image data is fully read from the source, no
 	// errors happened.
 	// errors happened.
 	resultSize, err := resultData.Size()
 	resultSize, err := resultData.Size()
 	if err != nil {
 	if err != nil {
-		return ierrors.Wrap(err, 0, ierrors.WithCategory(categoryImageDataSize))
+		return ierrors.Wrap(err, 0, ierrors.WithCategory(handlers.CategoryImageDataSize))
 	}
 	}
 
 
-	r.hwr.SetContentType(resultData.Format().Mime())
-	r.hwr.SetContentLength(resultSize)
-	r.hwr.SetContentDisposition(
-		r.imageURL,
-		r.po.Filename,
+	r.HeaderWriter.SetContentType(resultData.Format().Mime())
+	r.HeaderWriter.SetContentLength(resultSize)
+	r.HeaderWriter.SetContentDisposition(
+		r.ImageURL,
+		r.Options.Filename,
 		resultData.Format().Ext(),
 		resultData.Format().Ext(),
 		"",
 		"",
-		r.po.ReturnAttachment,
+		r.Options.ReturnAttachment,
 	)
 	)
-	r.hwr.SetExpires(r.po.Expires)
-	r.hwr.SetVary()
-	r.hwr.SetCanonical(r.imageURL)
+	r.HeaderWriter.SetExpires(r.Options.Expires)
+	r.HeaderWriter.SetVary()
+	r.HeaderWriter.SetCanonical(r.ImageURL)
 
 
-	if r.config.LastModifiedEnabled {
-		r.hwr.Passthrough(httpheaders.LastModified)
+	if r.Config.LastModifiedEnabled {
+		r.HeaderWriter.Passthrough(httpheaders.LastModified)
 	}
 	}
 
 
-	if r.config.ETagEnabled {
-		r.hwr.Passthrough(httpheaders.Etag)
+	if r.Config.ETagEnabled {
+		r.HeaderWriter.Passthrough(httpheaders.Etag)
 	}
 	}
 
 
-	r.hwr.Write(r.rw)
+	r.HeaderWriter.Write(r.ResponseWriter)
 
 
-	r.rw.WriteHeader(statusCode)
+	r.ResponseWriter.WriteHeader(statusCode)
 
 
-	_, err = io.Copy(r.rw, resultData.Reader())
+	_, err = io.Copy(r.ResponseWriter, resultData.Reader())
 
 
 	var ierr *ierrors.Error
 	var ierr *ierrors.Error
 	if err != nil {
 	if err != nil {
-		ierr = newResponseWriteError(err)
-
-		if r.config.ReportIOErrors {
-			return ierrors.Wrap(ierr, 0, ierrors.WithCategory(categoryIO), ierrors.WithShouldReport(true))
+		ierr = handlers.NewResponseWriteError(err)
+
+		if r.Config.ReportIOErrors {
+			return ierrors.Wrap(
+				ierr, 0,
+				ierrors.WithCategory(handlers.CategoryIO),
+				ierrors.WithShouldReport(true),
+			)
 		}
 		}
 	}
 	}
 
 
 	server.LogResponse(
 	server.LogResponse(
-		r.reqID, r.imageRequest, statusCode, ierr,
+		r.ID, r.Req, statusCode, ierr,
 		log.Fields{
 		log.Fields{
-			"image_url":          r.imageURL,
-			"processing_options": r.po,
+			"image_url":          r.ImageURL,
+			"processing_options": r.Options,
 		},
 		},
 	)
 	)
 
 
 	return nil
 	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
-}

+ 178 - 0
handlers/request.go

@@ -0,0 +1,178 @@
+package handlers
+
+import (
+	"context"
+	"net/http"
+
+	"github.com/imgproxy/imgproxy/v3/cookies"
+	"github.com/imgproxy/imgproxy/v3/headerwriter"
+	"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/security"
+	"github.com/imgproxy/imgproxy/v3/server"
+	"github.com/imgproxy/imgproxy/v3/structdiff"
+)
+
+// Options is an object of URL options extracted from the URL
+type Options = structdiff.Diffable
+
+// PathPaser is an interface for URL path parser: it extracts processing options and image path
+type Constructor[O Options] interface {
+	ParsePath(path string, headers http.Header) (O, string, error)
+	CreateMeta(ctx context.Context, imageURL string, po O) monitoring.Meta
+}
+
+type Request struct {
+	Context        Context               // Input context interface
+	Config         *Config               // Handler configuration
+	ID             string                // Request ID
+	Req            *http.Request         // Original HTTP request
+	ResponseWriter http.ResponseWriter   // HTTP response writer
+	HeaderWriter   *headerwriter.Request // Header writer request
+	ImageURL       string                // Image URL to process
+	MonitoringMeta monitoring.Meta       // Monitoring metadata
+}
+
+// PrepareRequest extracts image url and processing options from request URL and verifies them
+func NewRequest[P Constructor[O], O Options](
+	handler Context, // or, essentially, instance
+	constructor P,
+	imageRequest *http.Request,
+	config *Config,
+	reqID string,
+	rw http.ResponseWriter,
+) (*Request, O, error) {
+	// let's extract signature and valid request path from a request
+	path, signature, err := splitPathSignature(imageRequest, config)
+	if err != nil {
+		return nil, *new(O), err
+	}
+
+	// verify the signature (if any)
+	if err = security.VerifySignature(signature, path); err != nil {
+		return nil, *new(O), ierrors.Wrap(err, 0, ierrors.WithCategory(CategorySecurity))
+	}
+
+	// parse image url and processing options
+	po, imageURL, err := constructor.ParsePath(path, imageRequest.Header)
+	if err != nil {
+		return nil, *new(O), ierrors.Wrap(err, 0, ierrors.WithCategory(CategoryPathParsing))
+	}
+
+	mm := constructor.CreateMeta(imageRequest.Context(), imageURL, po)
+
+	// verify that image URL came from the valid source
+	err = security.VerifySourceURL(imageURL)
+	if err != nil {
+		return nil, *new(O), ierrors.Wrap(err, 0, ierrors.WithCategory(CategorySecurity))
+	}
+
+	return &Request{
+		Context:        handler,
+		Config:         config,
+		ID:             reqID,
+		Req:            imageRequest,
+		ResponseWriter: rw,
+		HeaderWriter:   handler.HeaderWriter().NewRequest(),
+		ImageURL:       imageURL,
+		MonitoringMeta: mm,
+	}, po, nil
+}
+
+// MakeDownloadOptions creates [imagedata.DownloadOptions]
+// from image request headers and security options.
+func (r *Request) MakeDownloadOptions(
+	h http.Header,
+	secops security.Options,
+) imagedata.DownloadOptions {
+	return imagedata.DownloadOptions{
+		Header:         h,
+		MaxSrcFileSize: secops.MaxSrcFileSize,
+	}
+}
+
+// AcquireProcessingSem acquires the processing semaphore.
+// It allows as many concurrent processing requests as workers are configured.
+func (r *Request) AcquireProcessingSem(ctx context.Context) (context.CancelFunc, error) {
+	defer monitoring.StartQueueSegment(ctx)()
+
+	sem := r.Context.Semaphores()
+
+	// Acquire queue semaphore (if enabled)
+	releaseQueueSem, err := sem.AcquireQueue()
+	if err != nil {
+		return nil, err
+	}
+	// Defer releasing the queue semaphore since we'll exit the queue on return
+	defer releaseQueueSem()
+
+	// Acquire processing semaphore
+	releaseProcessingSem, err := sem.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 releaseProcessingSem, nil
+}
+
+// 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.Req.Header.Get(httpheaders.IfNoneMatch))
+	}
+
+	// If LastModified is enabled, we forward If-Modified-Since header
+	if r.Config.LastModifiedEnabled {
+		h.Set(httpheaders.IfModifiedSince, r.Req.Header.Get(httpheaders.IfModifiedSince))
+	}
+
+	return h
+}
+
+// FetchImage downloads the source image asynchronously
+func (r *Request) FetchImage(
+	ctx context.Context,
+	do imagedata.DownloadOptions,
+) (imagedata.ImageData, http.Header, error) {
+	do.DownloadFinished = monitoring.StartDownloadingSegment(ctx, r.MonitoringMeta.Filter(
+		monitoring.MetaSourceImageURL,
+		monitoring.MetaSourceImageOrigin,
+	))
+
+	var err error
+
+	if r.Config.CookiePassthrough {
+		do.CookieJar, err = cookies.JarFromRequest(r.Req)
+		if err != nil {
+			return nil, nil, ierrors.Wrap(err, 0, ierrors.WithCategory(CategoryDownload))
+		}
+	}
+
+	return r.Context.ImageDataFactory().DownloadAsync(ctx, r.ImageURL, "source image", do)
+}
+
+// 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
+}

+ 53 - 27
imgproxy.go

@@ -31,19 +31,21 @@ const (
 
 
 // ImgProxy holds all the components needed for imgproxy to function
 // ImgProxy holds all the components needed for imgproxy to function
 type ImgProxy struct {
 type ImgProxy struct {
-	HeaderWriter      *headerwriter.Writer
-	Semaphores        *semaphores.Semaphores
-	FallbackImage     auximageprovider.Provider
-	WatermarkImage    auximageprovider.Provider
-	Fetcher           *fetcher.Fetcher
-	ProcessingHandler *processinghandler.Handler
-	StreamHandler     *stream.Handler
-	ImageDataFactory  *imagedata.Factory
-	Config            *Config
+	headerWriter      *headerwriter.Writer
+	semaphores        *semaphores.Semaphores
+	fallbackImage     auximageprovider.Provider
+	watermarkImage    auximageprovider.Provider
+	fetcher           *fetcher.Fetcher
+	processingHandler *processinghandler.Handler
+	streamHandler     *stream.Handler
+	imageDataFactory  *imagedata.Factory
+	config            *Config
 }
 }
 
 
 // New creates a new imgproxy instance
 // New creates a new imgproxy instance
 func New(ctx context.Context, config *Config) (*ImgProxy, error) {
 func New(ctx context.Context, config *Config) (*ImgProxy, error) {
+	i := &ImgProxy{}
+
 	headerWriter, err := headerwriter.New(&config.HeaderWriter)
 	headerWriter, err := headerwriter.New(&config.HeaderWriter)
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
@@ -82,7 +84,7 @@ func New(ctx context.Context, config *Config) (*ImgProxy, error) {
 	}
 	}
 
 
 	ph, err := processinghandler.New(
 	ph, err := processinghandler.New(
-		streamHandler, headerWriter, semaphores, fallbackImage, watermarkImage, idf, &config.ProcessingHandler,
+		i, streamHandler, &config.ProcessingHandler,
 	)
 	)
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
@@ -103,22 +105,22 @@ func New(ctx context.Context, config *Config) (*ImgProxy, error) {
 		return nil, err
 		return nil, err
 	}
 	}
 
 
-	return &ImgProxy{
-		HeaderWriter:      headerWriter,
-		Semaphores:        semaphores,
-		FallbackImage:     fallbackImage,
-		WatermarkImage:    watermarkImage,
-		Fetcher:           fetcher,
-		StreamHandler:     streamHandler,
-		ProcessingHandler: ph,
-		ImageDataFactory:  idf,
-		Config:            config,
-	}, nil
+	i.headerWriter = headerWriter
+	i.semaphores = semaphores
+	i.fallbackImage = fallbackImage
+	i.watermarkImage = watermarkImage
+	i.fetcher = fetcher
+	i.processingHandler = ph
+	i.streamHandler = streamHandler
+	i.imageDataFactory = idf
+	i.config = config
+
+	return i, nil
 }
 }
 
 
 // BuildRouter sets up the HTTP routes and middleware
 // BuildRouter sets up the HTTP routes and middleware
 func (i *ImgProxy) BuildRouter() (*server.Router, error) {
 func (i *ImgProxy) BuildRouter() (*server.Router, error) {
-	r, err := server.NewRouter(&i.Config.Server)
+	r, err := server.NewRouter(&i.config.Server)
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
@@ -128,12 +130,12 @@ func (i *ImgProxy) BuildRouter() (*server.Router, error) {
 
 
 	r.GET(faviconPath, r.NotFoundHandler).Silent()
 	r.GET(faviconPath, r.NotFoundHandler).Silent()
 	r.GET(healthPath, handlers.HealthHandler).Silent()
 	r.GET(healthPath, handlers.HealthHandler).Silent()
-	if i.Config.Server.HealthCheckPath != "" {
-		r.GET(i.Config.Server.HealthCheckPath, handlers.HealthHandler).Silent()
+	if i.config.Server.HealthCheckPath != "" {
+		r.GET(i.config.Server.HealthCheckPath, handlers.HealthHandler).Silent()
 	}
 	}
 
 
 	r.GET(
 	r.GET(
-		"/*", i.ProcessingHandler.Execute,
+		"/*", i.processingHandler.Execute,
 		r.WithSecret, r.WithCORS, r.WithPanic, r.WithReportError, r.WithMonitoring,
 		r.WithSecret, r.WithCORS, r.WithPanic, r.WithReportError, r.WithMonitoring,
 	)
 	)
 
 
@@ -171,7 +173,7 @@ func (i *ImgProxy) StartServer(ctx context.Context) error {
 
 
 // startMemoryTicker starts a ticker that periodically frees memory and optionally logs memory stats
 // startMemoryTicker starts a ticker that periodically frees memory and optionally logs memory stats
 func (i *ImgProxy) startMemoryTicker(ctx context.Context) {
 func (i *ImgProxy) startMemoryTicker(ctx context.Context) {
-	ticker := time.NewTicker(i.Config.Server.FreeMemoryInterval)
+	ticker := time.NewTicker(i.config.Server.FreeMemoryInterval)
 	defer ticker.Stop()
 	defer ticker.Stop()
 
 
 	for {
 	for {
@@ -181,9 +183,33 @@ func (i *ImgProxy) startMemoryTicker(ctx context.Context) {
 		case <-ticker.C:
 		case <-ticker.C:
 			memory.Free()
 			memory.Free()
 
 
-			if i.Config.Server.LogMemStats {
+			if i.config.Server.LogMemStats {
 				memory.LogStats()
 				memory.LogStats()
 			}
 			}
 		}
 		}
 	}
 	}
 }
 }
+
+func (i *ImgProxy) HeaderWriter() *headerwriter.Writer {
+	return i.headerWriter
+}
+
+func (i *ImgProxy) Semaphores() *semaphores.Semaphores {
+	return i.semaphores
+}
+
+func (i *ImgProxy) FallbackImage() auximageprovider.Provider {
+	return i.fallbackImage
+}
+
+func (i *ImgProxy) WatermarkImage() auximageprovider.Provider {
+	return i.watermarkImage
+}
+
+func (i *ImgProxy) Fetcher() *fetcher.Fetcher {
+	return i.fetcher
+}
+
+func (i *ImgProxy) ImageDataFactory() *imagedata.Factory {
+	return i.imageDataFactory
+}

+ 1 - 1
integration/processing_handler_test.go

@@ -267,7 +267,7 @@ func (s *ProcessingHandlerTestSuite) TestSkipProcessingSVG() {
 
 
 	s.Require().Equal(http.StatusOK, res.StatusCode)
 	s.Require().Equal(http.StatusOK, res.StatusCode)
 
 
-	data, err := s.imgproxy().ImageDataFactory.NewFromBytes(s.testData.Read("test1.svg"))
+	data, err := s.imgproxy().ImageDataFactory().NewFromBytes(s.testData.Read("test1.svg"))
 	s.Require().NoError(err)
 	s.Require().NoError(err)
 
 
 	expected, err := svg.Sanitize(data)
 	expected, err := svg.Sanitize(data)