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"
)
// 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
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
HandlerContext
stream *stream.Handler // Stream handler for raw image streaming
config *Config // Handler configuration
}
// New creates new handler object
func New(
hCtx HandlerContext,
stream *stream.Handler,
hw *headerwriter.Writer,
semaphores *semaphores.Semaphores,
fi auximageprovider.Provider,
wi auximageprovider.Provider,
idf *imagedata.Factory,
config *Config,
) (*Handler, error) {
if err := config.Validate(); err != nil {
@@ -45,13 +47,9 @@ func New(
}
return &Handler{
hw: hw,
HandlerContext: hCtx,
config: config,
stream: stream,
semaphores: semaphores,
fallbackImage: fi,
watermarkImage: wi,
idf: idf,
}, nil
}
@@ -59,49 +57,48 @@ func New(
func (h *Handler) Execute(
reqID string,
rw http.ResponseWriter,
imageRequest *http.Request,
req *http.Request,
) error {
// Increment the number of requests in progress
stats.IncRequestsInProgress()
defer stats.DecRequestsInProgress()
ctx := imageRequest.Context()
ctx := req.Context()
// 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 {
return err
}
// if processing options indicate raw image streaming, stream it and return
if po.Raw {
return h.stream.Execute(ctx, imageRequest, imageURL, reqID, po, rw)
return h.stream.Execute(ctx, req, imageURL, reqID, po, rw)
}
req := &request{
handler: h,
imageRequest: imageRequest,
hReq := &request{
HandlerContext: h,
reqID: reqID,
req: req,
rw: rw,
config: h.config,
po: po,
imageURL: imageURL,
monitoringMeta: mm,
semaphores: h.semaphores,
hwr: h.hw.NewRequest(),
idf: h.idf,
hwr: h.HeaderWriter().NewRequest(),
}
return req.execute(ctx)
return hReq.execute(ctx)
}
// newRequest extracts image url and processing options from request URL and verifies them
func (h *Handler) newRequest(
ctx context.Context,
imageRequest *http.Request,
req *http.Request,
) (string, *options.ProcessingOptions, monitoring.Meta, error) {
// 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 {
return "", nil, nil, err
}
@@ -112,7 +109,7 @@ func (h *Handler) newRequest(
}
// 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 {
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
errorreport.SetMetadata(imageRequest, "Source Image URL", imageURL)
errorreport.SetMetadata(imageRequest, "Source Image Origin", imageOrigin)
errorreport.SetMetadata(imageRequest, "Processing Options", po)
errorreport.SetMetadata(req, "Source Image URL", imageURL)
errorreport.SetMetadata(req, "Source Image Origin", imageOrigin)
errorreport.SetMetadata(req, "Processing Options", po)
monitoring.SetMetadata(ctx, mm)

View File

@@ -9,29 +9,26 @@ import (
"github.com/imgproxy/imgproxy/v3/handlers"
"github.com/imgproxy/imgproxy/v3/headerwriter"
"github.com/imgproxy/imgproxy/v3/ierrors"
"github.com/imgproxy/imgproxy/v3/imagedata"
"github.com/imgproxy/imgproxy/v3/imagetype"
"github.com/imgproxy/imgproxy/v3/monitoring"
"github.com/imgproxy/imgproxy/v3/monitoring/stats"
"github.com/imgproxy/imgproxy/v3/options"
"github.com/imgproxy/imgproxy/v3/semaphores"
"github.com/imgproxy/imgproxy/v3/server"
"github.com/imgproxy/imgproxy/v3/vips"
)
// request holds the parameters and state for a single request request
type request struct {
handler *Handler
imageRequest *http.Request
HandlerContext
reqID string
req *http.Request
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
@@ -46,7 +43,7 @@ func (r *request) execute(ctx context.Context) error {
}
// Acquire queue semaphore (if enabled)
releaseQueueSem, err := r.semaphores.AcquireQueue()
releaseQueueSem, err := r.Semaphores().AcquireQueue()
if err != nil {
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 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 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
@@ -40,7 +40,7 @@ func (r *request) makeImageRequestHeaders() http.Header {
func (r *request) acquireProcessingSem(ctx context.Context) (context.CancelFunc, error) {
defer monitoring.StartQueueSegment(ctx)()
fn, err := r.semaphores.AcquireProcessing(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
@@ -76,13 +76,13 @@ func (r *request) fetchImage(ctx context.Context, do imagedata.DownloadOptions)
var err error
if r.config.CookiePassthrough {
do.CookieJar, err = cookies.JarFromRequest(r.imageRequest)
do.CookieJar, err = cookies.JarFromRequest(r.req)
if err != nil {
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
@@ -103,7 +103,7 @@ func (r *request) handleDownloadError(
// We didn't return, so we have to report error
if err.ShouldReport() {
errorreport.Report(err, r.imageRequest)
errorreport.Report(err, r.req)
}
log.
@@ -134,16 +134,18 @@ func (r *request) getFallbackImage(
ctx context.Context,
po *options.ProcessingOptions,
) (imagedata.ImageData, http.Header) {
if r.handler.fallbackImage == nil {
fbi := r.FallbackImage()
if fbi == nil {
return nil, nil
}
data, h, err := r.handler.fallbackImage.Get(ctx, po)
data, h, err := fbi.Get(ctx, po)
if err != nil {
log.Warning(err.Error())
if ierr := r.wrapDownloadingErr(err); ierr.ShouldReport() {
errorreport.Report(ierr, r.imageRequest)
errorreport.Report(ierr, r.req)
}
return nil, nil
@@ -155,7 +157,7 @@ func (r *request) getFallbackImage(
// 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)
return processing.ProcessImage(ctx, originData, r.po, r.WatermarkImage())
}
// 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)
server.LogResponse(
r.reqID, r.imageRequest, http.StatusNotModified, nil,
r.reqID, r.req, http.StatusNotModified, nil,
log.Fields{
"image_url": r.imageURL,
"processing_options": r.po,
@@ -256,7 +258,7 @@ func (r *request) respondWithImage(statusCode int, resultData imagedata.ImageDat
}
server.LogResponse(
r.reqID, r.imageRequest, statusCode, ierr,
r.reqID, r.req, statusCode, ierr,
log.Fields{
"image_url": r.imageURL,
"processing_options": r.po,

View File

@@ -34,14 +34,14 @@ type ImgproxyHandlers struct {
// Imgproxy holds all the components needed for imgproxy to function
type Imgproxy struct {
HeaderWriter *headerwriter.Writer
Semaphores *semaphores.Semaphores
FallbackImage auximageprovider.Provider
WatermarkImage auximageprovider.Provider
Fetcher *fetcher.Fetcher
ImageDataFactory *imagedata.Factory
Handlers ImgproxyHandlers
Config *Config
headerWriter *headerwriter.Writer
semaphores *semaphores.Semaphores
fallbackImage auximageprovider.Provider
watermarkImage auximageprovider.Provider
fetcher *fetcher.Fetcher
imageDataFactory *imagedata.Factory
handlers ImgproxyHandlers
config *Config
}
// New creates a new imgproxy instance
@@ -74,25 +74,25 @@ func New(ctx context.Context, config *Config) (*Imgproxy, error) {
}
imgproxy := &Imgproxy{
HeaderWriter: headerWriter,
Semaphores: semaphores,
FallbackImage: fallbackImage,
WatermarkImage: watermarkImage,
Fetcher: fetcher,
ImageDataFactory: idf,
Config: config,
headerWriter: headerWriter,
semaphores: semaphores,
fallbackImage: fallbackImage,
watermarkImage: watermarkImage,
fetcher: fetcher,
imageDataFactory: idf,
config: config,
}
imgproxy.Handlers.Health = healthhandler.New()
imgproxy.Handlers.Landing = landinghandler.New()
imgproxy.handlers.Health = healthhandler.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 {
return nil, err
}
imgproxy.Handlers.Processing, err = processinghandler.New(
imgproxy.Handlers.Stream, headerWriter, semaphores, fallbackImage, watermarkImage, idf, &config.Handlers.Processing,
imgproxy.handlers.Processing, err = processinghandler.New(
imgproxy, imgproxy.handlers.Stream, &config.Handlers.Processing,
)
if err != nil {
return nil, err
@@ -103,22 +103,22 @@ func New(ctx context.Context, config *Config) (*Imgproxy, error) {
// BuildRouter sets up the HTTP routes and middleware
func (i *Imgproxy) BuildRouter() (*server.Router, error) {
r, err := server.NewRouter(&i.Config.Server)
r, err := server.NewRouter(&i.config.Server)
if err != nil {
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(healthPath, i.Handlers.Health.Execute).Silent()
if i.Config.Server.HealthCheckPath != "" {
r.GET(i.Config.Server.HealthCheckPath, i.Handlers.Health.Execute).Silent()
r.GET(healthPath, i.handlers.Health.Execute).Silent()
if i.config.Server.HealthCheckPath != "" {
r.GET(i.config.Server.HealthCheckPath, i.handlers.Health.Execute).Silent()
}
r.GET(
"/*", i.Handlers.Processing.Execute,
"/*", i.handlers.Processing.Execute,
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
func (i *Imgproxy) startMemoryTicker(ctx context.Context) {
ticker := time.NewTicker(i.Config.Server.FreeMemoryInterval)
ticker := time.NewTicker(i.config.Server.FreeMemoryInterval)
defer ticker.Stop()
for {
@@ -173,9 +173,29 @@ func (i *Imgproxy) startMemoryTicker(ctx context.Context) {
case <-ticker.C:
memory.Free()
if i.Config.Server.LogMemStats {
if i.config.Server.LogMemStats {
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
}