Help us shape the future of Go! Take the 2026 Go Developer Survey

imageoptimizer

package module
v0.0.0-...-58f4bec Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 25, 2026 License: MIT Imports: 34 Imported by: 0

README

caddy-image-optimizer

A Caddy HTTP handler that resizes and re-encodes images on demand and caches the results on disk. It serves JPEG, PNG and WebP files from your site root, picks WebP automatically for browsers that accept it, and leaves everything else to file_server.

GET /photo.jpg?w=640            -> 640px-wide JPEG
GET /photo.jpg  (Accept: image/webp) -> WebP, same dimensions
GET /photo.jpg?preset=thumb     -> whatever "thumb" is defined as

Image work is done by libvips through govips. libvips decodes only the pixels it needs (shrink-on-load), so resizing a large JPEG stays cheap.

Features

  • Resize with w and h (a bounding box, aspect ratio preserved, never upscaled), set quality with q, choose the output format with fmt.
  • Output formats: WebP, JPEG, PNG. Without fmt, the format is negotiated from the Accept header (WebP if the client accepts it, otherwise the source format).
  • Named presets, and a presets_only mode that disables raw parameters so the set of variants is fixed by your config.
  • Allowlists for widths, heights, qualities and formats, with a configurable response to disallowed values: reject (400), snap to the nearest allowed value, or serve the original.
  • Disk cache with a size cap and LRU eviction. Cache entries are keyed on source path, modification time, size and variant, so replacing a source file serves fresh output with no manual purge.
  • Identical concurrent requests share one transform. A concurrency cap and queue timeout protect the server, and requests over the cap get a 503 with Retry-After.
  • Correct HTTP caching: strong ETag, Last-Modified, conditional requests (304 without touching the cache), Range, and Vary: Accept wherever the response depends on Accept.
  • Safe by default: input type is checked by magic bytes, not file extension, dimensions are read from the header before any decode, and limits on source pixels and bytes apply. Metadata (EXIF) is stripped after the orientation is applied.
  • Animated GIF, APNG and animated WebP, unsupported types, missing files and requests that need no change all pass through to the next handler unchanged.

Requirements

  • Go 1.26 or newer to build.
  • libvips 8.10 or newer with development headers, and a C toolchain. The module uses CGO, so a plain go build or xcaddy build needs CGO_ENABLED=1 (the default when a C compiler is present).
    • Debian/Ubuntu: sudo apt install libvips-dev
    • Alpine: apk add vips-dev gcc musl-dev
    • macOS: brew install vips
  • libvips must also be installed on the machine that runs the resulting Caddy binary.

Build

xcaddy build --with github.com/btafoya/caddy-image-optimizer

Check that the module is present:

./caddy list-modules | grep image_optimizer
# http.handlers.image_optimizer

Quick start

example.com {
	root * /var/www
	image_optimizer {
		widths 320 640 960 1280
		qualities 40-90
		preset thumb w=320 q=70 fmt=webp
	}
	file_server
}

The image_optimizer directive is ordered before file_server automatically. Inside a route block, place it yourself, before file_server.

Request parameters

Parameter Meaning
w Maximum width in pixels.
h Maximum height in pixels. With both w and h the image is scaled to fit inside the box; aspect ratio is always preserved and images are never enlarged.
q Quality, 1 to 100. Defaults to 80 for JPEG and 75 for WebP. Ignored for PNG.
fmt jpg (or jpeg), png or webp. When absent, the format follows the Accept header.
preset Name of a configured preset. Explicit w, h, q and fmt override the preset's values unless presets_only is on.

Other query parameters are ignored, so cache-busting parameters like ?v=123 are safe. Malformed values (w=abc, q=500, fmt=gif) always return 400. Values that are well-formed but not allowed by your config follow on_disallowed.

Configuration

All options are optional.

image_optimizer {
	root            {http.vars.root}      # where source images live
	cache_dir       /var/cache/caddy/img  # default: <caddy data dir>/image_optimizer
	cache_max_size  2GiB                  # default: 1GiB

	widths          320 640 960 1280      # allowed widths; default: any up to max_width
	heights         480 720               # allowed heights; default: any up to max_height
	qualities       40-90                 # allowed q range; default: 1-100
	formats         webp jpg png          # allowed output formats; default: all three

	preset thumb    w=320 q=70 fmt=webp
	preset hero     w=1920 q=80
	presets_only                          # optional: disable raw w/h/q/fmt

	on_disallowed   reject                # reject | snap | bypass   (default: reject)
	on_fail         bypass                # bypass | abort           (default: bypass)
	cache_control   "public, max-age=604800"

	limits {
		max_source_pixels  50000000       # default: 50,000,000
		max_source_bytes   50MiB          # default: 50MiB
		max_width          4096           # used when widths is not set
		max_height         4096           # used when heights is not set
		max_concurrent     8              # default: number of CPUs
		queue_timeout      5s             # wait for a free slot, then 503
		transform_timeout  15s
	}
}
Behaviour of the options
  • on_disallowed: what happens when a width, height, quality, format or preset is well-formed but not allowed. reject returns 400. snap rounds a width or height up to the smallest allowed value at or above the request (or the largest allowed value), clamps quality into range, and falls back to negotiation for a disallowed format. bypass serves the original file.
  • on_fail: what happens when a transform itself fails. bypass serves the original file, abort returns 500.
  • cache_control: set on transformed responses only. If unset, the handler adds no Cache-Control; headers from other directives are untouched.
  • presets_only: any raw w, h, q or fmt is treated as disallowed and follows on_disallowed. Because the variants are fixed by your config, this is the strongest protection against cache flooding.
  • Oversize sources: a request with explicit parameters for a source above max_source_pixels or max_source_bytes returns 422. A request that only needs Accept negotiation gets the original instead.
JSON

The module ID is http.handlers.image_optimizer:

{
	"handler": "image_optimizer",
	"root": "/var/www",
	"widths": [320, 640, 960],
	"presets": { "thumb": { "w": 320, "q": 70, "fmt": "webp" } },
	"on_disallowed": "snap",
	"limits": { "max_concurrent": 4, "transform_timeout": "15s" }
}

Operational notes

  • Public sites should set widths (or use presets_only). Without an allowlist, every distinct w creates a separate cache entry. The cache cap and LRU eviction bound disk use, but an attacker can still churn the cache and burn CPU.
  • Caching proxies and CDNs. Responses that depend on Accept carry Vary: Accept. Use a CDN that honours it, or request an explicit fmt in your markup.
  • Several Caddy instances can share a cache directory safely (writes are atomic), but each keeps its own size accounting, so give each its own directory if you rely on the size cap.
  • Cache directory is created with mode 0700. Entries left over from an interrupted write are cleaned up at startup.
  • libvips cannot cancel a running transform. If one exceeds transform_timeout the request fails, but the transform finishes in the background and keeps its concurrency slot until it does.
  • Reloads. libvips is initialised once per process and never shut down, so libvips-level settings cannot change on a config reload.

Limitations

  • Local files only. There is no remote fetch and no reverse_proxy response processing.
  • Scale-to-fit only: no crop, cover or pad modes.
  • No AVIF output yet.
  • Animated images are served unchanged.
  • The cache is local to the machine and is not shared across hosts.

Development

go vet ./...
go test -race ./...

The tests need libvips installed. The tests cover parameter handling, caching, conditional requests, EXIF orientation, concurrency, limits and the Caddyfile parser. The design rationale is in DESIGN.md and the original requirements are in REQUIREMENTS.md.

License

MIT

Documentation

Overview

Package imageoptimizer is a Caddy handler that resizes and re-encodes JPEG/PNG/WebP files from the site root on demand, with a disk cache.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Limits

type Limits struct {
	MaxSourcePixels  int64          `json:"max_source_pixels,omitempty"`
	MaxSourceBytes   int64          `json:"max_source_bytes,omitempty"`
	MaxWidth         int            `json:"max_width,omitempty"`
	MaxHeight        int            `json:"max_height,omitempty"`
	MaxConcurrent    int            `json:"max_concurrent,omitempty"`
	QueueTimeout     caddy.Duration `json:"queue_timeout,omitempty"`
	TransformTimeout caddy.Duration `json:"transform_timeout,omitempty"`
}

Limits bounds the work a request can cause.

type Middleware

type Middleware struct {
	Root         string            `json:"root,omitempty"`
	CacheDir     string            `json:"cache_dir,omitempty"`
	CacheMaxSize int64             `json:"cache_max_size,omitempty"`
	Widths       []int             `json:"widths,omitempty"`
	Heights      []int             `json:"heights,omitempty"`
	QualityMin   int               `json:"quality_min,omitempty"`
	QualityMax   int               `json:"quality_max,omitempty"`
	Formats      []string          `json:"formats,omitempty"`
	Presets      map[string]Preset `json:"presets,omitempty"`
	PresetsOnly  bool              `json:"presets_only,omitempty"`
	OnDisallowed string            `json:"on_disallowed,omitempty"` // reject | snap | bypass
	OnFail       string            `json:"on_fail,omitempty"`       // bypass | abort
	CacheControl string            `json:"cache_control,omitempty"`
	Limits       Limits            `json:"limits,omitempty"`
	// contains filtered or unexported fields
}

Middleware is the http.handlers.image_optimizer module.

func (*Middleware) CaddyModule

func (*Middleware) CaddyModule() caddy.ModuleInfo

CaddyModule implements caddy.Module.

func (*Middleware) Provision

func (m *Middleware) Provision(ctx caddy.Context) error

Provision implements caddy.Provisioner.

func (*Middleware) ServeHTTP

func (m *Middleware) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error

ServeHTTP implements caddyhttp.MiddlewareHandler. Anything it does not transform goes to next (file_server), which also owns every 404.

func (*Middleware) UnmarshalCaddyfile

func (m *Middleware) UnmarshalCaddyfile(d *caddyfile.Dispenser) error

UnmarshalCaddyfile implements caddyfile.Unmarshaler.

func (*Middleware) Validate

func (m *Middleware) Validate() error

Validate implements caddy.Validator.

type Preset

type Preset struct {
	W   int    `json:"w,omitempty"`
	H   int    `json:"h,omitempty"`
	Q   int    `json:"q,omitempty"`
	Fmt string `json:"fmt,omitempty"`
}

Preset is a named, admin-defined variant. Zero fields are unset.

type Variant

type Variant struct {
	W, H, Q int
	Fmt     string
}

Variant is a fully normalized transform request: it is the cache identity.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL