Serving
URL grammars. The positional route is
/resize/{w}/{h}/{file}[@fmt]; 0 leaves an axis unconstrained, and
{file} may span directories. Setting OXIMG_OPTIONS_PREFIX mounts a
second route speaking the Cloudflare Images option grammar at that
prefix:
/image/width=750,quality=80/albums/2026/photo.pngwith width/height (1-8192; one suffices, the other axis follows
the aspect ratio), quality (1-100, applied to whichever format the
output resolves to; PNG output is lossless and ignores it), and
format (jpeg|png|webp|avif, or auto = the same Accept
negotiation as a bare positional URL, which also runs when format
is absent). Unknown or duplicate options answer 400 naming the key —
Cloudflare silently ignores unknown options, but a silently dropped
fit=cover changes the output, so the divergence is deliberate. The
filename is taken literally on this route (no @fmt token).
Sources. With OXIMG_SOURCE_BASE_URL unset, sources come from
IMAGES_DIR. Set it and the scheme selects the transport:
https://host/prefix— anonymous HTTP. Exposure prerequisite: no credentials are sent, so the origin must be anonymously readable; for an object-store bucket that means public objects, and anyone who can guess a path can fetch the original at full resolution, bypassing every resize/signing/CDN control in front.gs://bucket[/prefix]— a private GCS bucket, read directly with GCP-attached credentials (GKE Workload Identity, Cloud Run, and GCE metadata credentials; tokens cached and refreshed; boot fails closed with a clear message when no credentials are reachable).service_accountJSON keys are not supported — on GCP use Workload Identity, off GCP use the HTTP mode.s3://is planned (issue #11).
Remote sources are downloaded into a bounded buffer (OXIMG_MAX_SOURCE_BYTES)
before the request takes a CPU slot, so the origin round trip never
holds one — measured at ~50% of a permit’s hold time on a production
corpus before the split (issue #20/#22). Download concurrency has its
own bound, OXIMG_FETCH_CONCURRENCY, and local sources keep the
streaming decode (no buffering, the page cache serves the read).
Connection-level transients (reset, refused, DNS blips) are retried
once before any body bytes are consumed, and the gs:// mode also
retries 429/5xx SDK-style; oximg_upstream_retries_total counts both.
Format ceilings are part of the fit: WebP cannot express a side
past 16383 px, so a request whose output would exceed that is scaled
down until it fits, aspect ratio preserved — a 2000x19708 source asked
for width=1920 as WebP comes back 1663x16383. Tall single-column
images (infographics, long product pages) hit this routinely, and the
alternative is failing a request the format simply cannot serve at the
asked-for size. The returned image reports its own dimensions; other
output formats have no ceiling worth enforcing here (their limits sit
past OXIMG_MAX_SRC_PIXELS).
Error classes follow fault, not convenience: a source key that no
store can serve — past an object store’s key-length limit, or refused
by the origin as a malformed request (400/414) — answers 400, and
an absent object 404. Only a genuinely unwell upstream (connect
failure, reset, 5xx) answers 502, with slow origins split off as
504. This matters downstream: CDNs retry and fail over on 5xx but
pass 4xx through to their error cache, so misfiling a client error as
an upstream failure both inflates the 5xx rate an operator watches and
turns a crawler into origin load. oximg_upstream_fetch_total splits
the same way (rejected and not_found apart from error), so that
series stays a signal of upstream health. Over-length keys are refused
locally, without a round trip.
Source paths are validated component-wise — ./.. components,
empty components, \, ?, #, and control bytes answer 400. Local
sources also pass a symlink-containment check (a path resolving
outside IMAGES_DIR answers 404), and remote paths are re-encoded
segment-wise so a percent in a name is never double-decoded upstream.
URL signing (optional): set OXIMG_KEY and OXIMG_SALT (hex) to
require imgproxy-style signed URLs —
/{base64url(HMAC-SHA256(key, salt || path))}/resize/{w}/{h}/{file},
and the same scheme over {prefix}/{options}/{file} on the options
route. The signed path is the percent-decoded form, so one signature
covers every URL encoding of the same source.
CORS preflight: OPTIONS on an image route answers 204 with
Allow: GET, HEAD, OPTIONS, because a browser preflight requires a
2xx — a 405 fails it no matter what CORS headers a CDN attaches, since
the status itself is the blocker. Preflights are not signature-checked
(they perform no work and answer identically for every path; the GET
that follows still is). oximg does not emit the CORS response headers
themselves — Access-Control-Allow-Origin and friends come from
whatever fronts it. Other methods still answer 405.
Graceful shutdown: on SIGTERM (what docker stop, Kubernetes, and
Cloud Run send) or SIGINT the server stops accepting connections,
finishes in-flight requests, and exits 0. There is no drain timeout of
its own — the orchestrator’s grace period backstops a response that
never finishes, so allow a few seconds more than your slowest expected
encode.