Provide shared resources to handlers via HandlerContext interface

This commit is contained in:
DarthSim
2025-09-10 19:47:48 +03:00
committed by Sergei Aleksandrovich
parent adb4341713
commit 53645688fb
4 changed files with 98 additions and 82 deletions

View File

@@ -19,25 +19,27 @@ import (
"github.com/imgproxy/imgproxy/v3/semaphores" "github.com/imgproxy/imgproxy/v3/semaphores"
) )
// HandlerContext provides access to shared handler dependencies
type HandlerContext interface {
HeaderWriter() *headerwriter.Writer
Semaphores() *semaphores.Semaphores
FallbackImage() auximageprovider.Provider
WatermarkImage() auximageprovider.Provider
ImageDataFactory() *imagedata.Factory
}
// Handler handles image processing requests // Handler handles image processing requests
type Handler struct { type Handler struct {
hw *headerwriter.Writer // Configured HeaderWriter instance HandlerContext
stream *stream.Handler // Stream handler for raw image streaming
config *Config // Handler configuration stream *stream.Handler // Stream handler for raw image streaming
semaphores *semaphores.Semaphores config *Config // Handler configuration
fallbackImage auximageprovider.Provider
watermarkImage auximageprovider.Provider
idf *imagedata.Factory
} }
// New creates new handler object // New creates new handler object
func New( func New(
hCtx HandlerContext,
stream *stream.Handler, stream *stream.Handler,
hw *headerwriter.Writer,
semaphores *semaphores.Semaphores,
fi auximageprovider.Provider,
wi auximageprovider.Provider,
idf *imagedata.Factory,
config *Config, config *Config,
) (*Handler, error) { ) (*Handler, error) {
if err := config.Validate(); err != nil { if err := config.Validate(); err != nil {
@@ -45,13 +47,9 @@ func New(
} }
return &Handler{ return &Handler{
hw: hw, HandlerContext: hCtx,
config: config, config: config,
stream: stream, stream: stream,
semaphores: semaphores,
fallbackImage: fi,
watermarkImage: wi,
idf: idf,
}, nil }, nil
} }
@@ -59,49 +57,48 @@ func New(
func (h *Handler) Execute( func (h *Handler) Execute(
reqID string, reqID string,
rw http.ResponseWriter, rw http.ResponseWriter,
imageRequest *http.Request, req *http.Request,
) error { ) error {
// Increment the number of requests in progress // Increment the number of requests in progress
stats.IncRequestsInProgress() stats.IncRequestsInProgress()
defer stats.DecRequestsInProgress() defer stats.DecRequestsInProgress()
ctx := imageRequest.Context() ctx := req.Context()
// Verify URL signature and extract image url and processing options // Verify URL signature and extract image url and processing options
imageURL, po, mm, err := h.newRequest(ctx, imageRequest) imageURL, po, mm, err := h.newRequest(ctx, req)
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, req, imageURL, reqID, po, rw)
} }
req := &request{ hReq := &request{
handler: h, HandlerContext: h,
imageRequest: imageRequest,
reqID: reqID, reqID: reqID,
req: req,
rw: rw, rw: rw,
config: h.config, config: h.config,
po: po, po: po,
imageURL: imageURL, imageURL: imageURL,
monitoringMeta: mm, monitoringMeta: mm,
semaphores: h.semaphores, hwr: h.HeaderWriter().NewRequest(),
hwr: h.hw.NewRequest(),
idf: h.idf,
} }
return req.execute(ctx) return hReq.execute(ctx)
} }
// newRequest extracts image url and processing options from request URL and verifies them // newRequest extracts image url and processing options from request URL and verifies them
func (h *Handler) newRequest( func (h *Handler) newRequest(
ctx context.Context, ctx context.Context,
imageRequest *http.Request, req *http.Request,
) (string, *options.ProcessingOptions, monitoring.Meta, error) { ) (string, *options.ProcessingOptions, monitoring.Meta, error) {
// let's extract signature and valid request path from a request // let's extract signature and valid request path from a request
path, signature, err := handlers.SplitPathSignature(imageRequest) path, signature, err := handlers.SplitPathSignature(req)
if err != nil { if err != nil {
return "", nil, nil, err return "", nil, nil, err
} }
@@ -112,7 +109,7 @@ func (h *Handler) newRequest(
} }
// parse image url and processing options // parse image url and processing options
po, imageURL, err := options.ParsePath(path, imageRequest.Header) po, imageURL, err := options.ParsePath(path, req.Header)
if err != nil { if err != nil {
return "", nil, nil, ierrors.Wrap(err, 0, ierrors.WithCategory(handlers.CategoryPathParsing)) return "", nil, nil, ierrors.Wrap(err, 0, ierrors.WithCategory(handlers.CategoryPathParsing))
} }
@@ -127,9 +124,9 @@ func (h *Handler) newRequest(
} }
// set error reporting and monitoring context // set error reporting and monitoring context
errorreport.SetMetadata(imageRequest, "Source Image URL", imageURL) errorreport.SetMetadata(req, "Source Image URL", imageURL)
errorreport.SetMetadata(imageRequest, "Source Image Origin", imageOrigin) errorreport.SetMetadata(req, "Source Image Origin", imageOrigin)
errorreport.SetMetadata(imageRequest, "Processing Options", po) errorreport.SetMetadata(req, "Processing Options", po)
monitoring.SetMetadata(ctx, mm) monitoring.SetMetadata(ctx, mm)

View File

@@ -9,29 +9,26 @@ import (
"github.com/imgproxy/imgproxy/v3/handlers" "github.com/imgproxy/imgproxy/v3/handlers"
"github.com/imgproxy/imgproxy/v3/headerwriter" "github.com/imgproxy/imgproxy/v3/headerwriter"
"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"
"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/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 // request holds the parameters and state for a single request request
type request struct { type request struct {
handler *Handler HandlerContext
imageRequest *http.Request
reqID string reqID string
req *http.Request
rw http.ResponseWriter rw http.ResponseWriter
config *Config config *Config
po *options.ProcessingOptions po *options.ProcessingOptions
imageURL string imageURL string
monitoringMeta monitoring.Meta monitoringMeta monitoring.Meta
semaphores *semaphores.Semaphores
hwr *headerwriter.Request hwr *headerwriter.Request
idf *imagedata.Factory
} }
// execute handles the actual processing logic // execute handles the actual processing logic
@@ -46,7 +43,7 @@ func (r *request) execute(ctx context.Context) error {
} }
// Acquire queue semaphore (if enabled) // Acquire queue semaphore (if enabled)
releaseQueueSem, err := r.semaphores.AcquireQueue() releaseQueueSem, err := r.Semaphores().AcquireQueue()
if err != nil { if err != nil {
return err return err
} }

View File

@@ -25,12 +25,12 @@ func (r *request) makeImageRequestHeaders() http.Header {
// If ETag is enabled, we forward If-None-Match header // If ETag is enabled, we forward If-None-Match header
if r.config.ETagEnabled { if r.config.ETagEnabled {
h.Set(httpheaders.IfNoneMatch, r.imageRequest.Header.Get(httpheaders.IfNoneMatch)) h.Set(httpheaders.IfNoneMatch, r.req.Header.Get(httpheaders.IfNoneMatch))
} }
// If LastModified is enabled, we forward If-Modified-Since header // If LastModified is enabled, we forward If-Modified-Since header
if r.config.LastModifiedEnabled { if r.config.LastModifiedEnabled {
h.Set(httpheaders.IfModifiedSince, r.imageRequest.Header.Get(httpheaders.IfModifiedSince)) h.Set(httpheaders.IfModifiedSince, r.req.Header.Get(httpheaders.IfModifiedSince))
} }
return h return h
@@ -40,7 +40,7 @@ func (r *request) makeImageRequestHeaders() http.Header {
func (r *request) acquireProcessingSem(ctx context.Context) (context.CancelFunc, error) { func (r *request) acquireProcessingSem(ctx context.Context) (context.CancelFunc, error) {
defer monitoring.StartQueueSegment(ctx)() defer monitoring.StartQueueSegment(ctx)()
fn, err := r.semaphores.AcquireProcessing(ctx) fn, err := r.Semaphores().AcquireProcessing(ctx)
if err != nil { if err != nil {
// We don't actually need to check timeout here, // We don't actually need to check timeout here,
// but it's an easy way to check if this is an actual timeout // but it's an easy way to check if this is an actual timeout
@@ -76,13 +76,13 @@ func (r *request) fetchImage(ctx context.Context, do imagedata.DownloadOptions)
var err error var err error
if r.config.CookiePassthrough { if r.config.CookiePassthrough {
do.CookieJar, err = cookies.JarFromRequest(r.imageRequest) do.CookieJar, err = cookies.JarFromRequest(r.req)
if err != nil { if err != nil {
return nil, nil, ierrors.Wrap(err, 0, ierrors.WithCategory(handlers.CategoryDownload)) return nil, nil, ierrors.Wrap(err, 0, ierrors.WithCategory(handlers.CategoryDownload))
} }
} }
return r.idf.DownloadAsync(ctx, r.imageURL, "source image", do) return r.ImageDataFactory().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
@@ -103,7 +103,7 @@ func (r *request) handleDownloadError(
// 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.
@@ -134,16 +134,18 @@ func (r *request) getFallbackImage(
ctx context.Context, ctx context.Context,
po *options.ProcessingOptions, po *options.ProcessingOptions,
) (imagedata.ImageData, http.Header) { ) (imagedata.ImageData, http.Header) {
if r.handler.fallbackImage == nil { fbi := r.FallbackImage()
if fbi == nil {
return nil, nil return nil, nil
} }
data, h, err := r.handler.fallbackImage.Get(ctx, po) data, h, err := fbi.Get(ctx, po)
if err != nil { if err != nil {
log.Warning(err.Error()) log.Warning(err.Error())
if ierr := r.wrapDownloadingErr(err); ierr.ShouldReport() { if ierr := r.wrapDownloadingErr(err); ierr.ShouldReport() {
errorreport.Report(ierr, r.imageRequest) errorreport.Report(ierr, r.req)
} }
return nil, nil return nil, nil
@@ -155,7 +157,7 @@ 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) { func (r *request) processImage(ctx context.Context, originData imagedata.ImageData) (*processing.Result, error) {
defer monitoring.StartProcessingSegment(ctx, r.monitoringMeta.Filter(monitoring.MetaProcessingOptions))() defer monitoring.StartProcessingSegment(ctx, r.monitoringMeta.Filter(monitoring.MetaProcessingOptions))()
return processing.ProcessImage(ctx, originData, r.po, r.handler.watermarkImage) return processing.ProcessImage(ctx, originData, r.po, r.WatermarkImage())
} }
// writeDebugHeaders writes debug headers (X-Origin-*, X-Result-*) to the response // writeDebugHeaders writes debug headers (X-Origin-*, X-Result-*) to the response
@@ -200,7 +202,7 @@ func (r *request) respondWithNotModified() error {
r.rw.WriteHeader(http.StatusNotModified) r.rw.WriteHeader(http.StatusNotModified)
server.LogResponse( server.LogResponse(
r.reqID, r.imageRequest, http.StatusNotModified, nil, r.reqID, r.req, http.StatusNotModified, nil,
log.Fields{ log.Fields{
"image_url": r.imageURL, "image_url": r.imageURL,
"processing_options": r.po, "processing_options": r.po,
@@ -256,7 +258,7 @@ func (r *request) respondWithImage(statusCode int, resultData imagedata.ImageDat
} }
server.LogResponse( server.LogResponse(
r.reqID, r.imageRequest, statusCode, ierr, r.reqID, r.req, statusCode, ierr,
log.Fields{ log.Fields{
"image_url": r.imageURL, "image_url": r.imageURL,
"processing_options": r.po, "processing_options": r.po,

View File

@@ -34,14 +34,14 @@ type ImgproxyHandlers struct {
// 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 headerWriter *headerwriter.Writer
Semaphores *semaphores.Semaphores semaphores *semaphores.Semaphores
FallbackImage auximageprovider.Provider fallbackImage auximageprovider.Provider
WatermarkImage auximageprovider.Provider watermarkImage auximageprovider.Provider
Fetcher *fetcher.Fetcher fetcher *fetcher.Fetcher
ImageDataFactory *imagedata.Factory imageDataFactory *imagedata.Factory
Handlers ImgproxyHandlers handlers ImgproxyHandlers
Config *Config config *Config
} }
// New creates a new imgproxy instance // New creates a new imgproxy instance
@@ -74,25 +74,25 @@ func New(ctx context.Context, config *Config) (*Imgproxy, error) {
} }
imgproxy := &Imgproxy{ imgproxy := &Imgproxy{
HeaderWriter: headerWriter, headerWriter: headerWriter,
Semaphores: semaphores, semaphores: semaphores,
FallbackImage: fallbackImage, fallbackImage: fallbackImage,
WatermarkImage: watermarkImage, watermarkImage: watermarkImage,
Fetcher: fetcher, fetcher: fetcher,
ImageDataFactory: idf, imageDataFactory: idf,
Config: config, config: config,
} }
imgproxy.Handlers.Health = healthhandler.New() imgproxy.handlers.Health = healthhandler.New()
imgproxy.Handlers.Landing = landinghandler.New() imgproxy.handlers.Landing = landinghandler.New()
imgproxy.Handlers.Stream, err = streamhandler.New(&config.Handlers.Stream, headerWriter, fetcher) imgproxy.handlers.Stream, err = streamhandler.New(&config.Handlers.Stream, headerWriter, fetcher)
if err != nil { if err != nil {
return nil, err return nil, err
} }
imgproxy.Handlers.Processing, err = processinghandler.New( imgproxy.handlers.Processing, err = processinghandler.New(
imgproxy.Handlers.Stream, headerWriter, semaphores, fallbackImage, watermarkImage, idf, &config.Handlers.Processing, imgproxy, imgproxy.handlers.Stream, &config.Handlers.Processing,
) )
if err != nil { if err != nil {
return nil, err return nil, err
@@ -103,22 +103,22 @@ func New(ctx context.Context, config *Config) (*Imgproxy, error) {
// 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
} }
r.GET("/", i.Handlers.Landing.Execute) r.GET("/", i.handlers.Landing.Execute)
r.GET("", i.Handlers.Landing.Execute) r.GET("", i.handlers.Landing.Execute)
r.GET(faviconPath, r.NotFoundHandler).Silent() r.GET(faviconPath, r.NotFoundHandler).Silent()
r.GET(healthPath, i.Handlers.Health.Execute).Silent() r.GET(healthPath, i.handlers.Health.Execute).Silent()
if i.Config.Server.HealthCheckPath != "" { if i.config.Server.HealthCheckPath != "" {
r.GET(i.Config.Server.HealthCheckPath, i.Handlers.Health.Execute).Silent() r.GET(i.config.Server.HealthCheckPath, i.handlers.Health.Execute).Silent()
} }
r.GET( r.GET(
"/*", i.Handlers.Processing.Execute, "/*", i.handlers.Processing.Execute,
r.WithSecret, r.WithCORS, r.WithPanic, r.WithReportError, r.WithMonitoring, r.WithSecret, r.WithCORS, r.WithPanic, r.WithReportError, r.WithMonitoring,
) )
@@ -163,7 +163,7 @@ func (i *Imgproxy) StartServer(ctx context.Context, hasStarted chan net.Addr) er
// 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 {
@@ -173,9 +173,29 @@ 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) ImageDataFactory() *imagedata.Factory {
return i.imageDataFactory
}