Skip to content

HTTP interface

The contract for iiiris's HTTP surface: the routes it serves, the middleware every request passes through, the status codes it returns, and the caching/CORS/conditional-GET semantics on every response. This is the transport-layer contract — distinct from the IIIF parameter grammar (iiif-compliance.md), the decode pipeline (image-decode.md), and the auth service shapes (auth.md).

What it does

A stdlib net/http.ServeMux routes requests through a fixed middleware chain to version-specific IIIF handlers (plus health, readiness, and admin endpoints). The IIIF handlers reconstruct the request path and delegate grammar parsing to iiif/v{2,3}.ParsePath, then run the redirect → auth → cache → render flow. Every IIIF response is emitted through http.ServeContent so conditional GETs, ranges, and ETags work uniformly.

Surface

Routes

Route Handler
GET /{$} Landing page (HTML).
GET /health Liveness — always 200 while the process is up.
GET /ready Readiness — runs every source's Healthchecker in parallel; 200 if all pass, 503 if any fails.
GET /iiif/3/{rest...} · GET /iiif/2/{rest...} Catch-all image + info.json requests; the handler splits {rest} into identifier + IIIF tail and calls the v3/v2 parser.
/admin/ Operator UI (HTTP Basic). Returns 404 when admin credentials are unset (the subsystem is off, not merely locked).
/iiif/auth/... The IIIF Auth 2.0/1.0 services — mounted only when auth profiles are configured (auth.md).

Middleware chain (outer → inner)

collapseSlashes → request counter + per-route stats → request log → optional per-IP concurrency cap → panic recoverer → handler.

  • collapseSlashes folds runs of / in the path before routing, so //iiif///3/... routes identically.
  • Per-route stats feed the /admin/ dashboard (count + latency percentiles per classified route).
  • Per-IP cap is wired only when image.max_concurrent_per_ip > 0; it sits inside count+log (so a rejection is counted and logged) and outside the recoverer.
  • Recoverer turns a handler panic into a 500 (logged), never a dropped connection.

Contracts

Status codes

Condition Status
Path-syntax or parameter-value rejection (bad region/size/rotation/quality, region origin outside source, upscale without ^) 400
image.max_pixel_area (source) or max_output_area (output) guard exceeded 400
Source rejects the identifier as malformed (source.ErrInvalidIdentifier) 400
Auth denied (hard deny) 401, or as supplied by the Authorizer
Identifier not found in the source 404
Per-IP concurrency cap exceeded 429
Decode / encode / source-I/O failure 500 (also a recovered panic)
A source healthcheck fails (on /ready) 503
Hook.Redirect returns a URL, or an auth Decision.Redirect 303
If-None-Match / If-Modified-Since matches the served entry 304

Status mapping is single-sourced in the handlers; the parameter-level detail behind a 400 is in iiif-compliance.md.

Error bodies are fixed strings — invalid identifier, not found, internal server error — never the underlying error text. A source error names the identifier, and for HTTP/S3 the upstream URL or bucket and key; echoing that back hands a prober its own input plus deployment detail. The error goes to the log (Warn for a rejected identifier, Error for a fault) and to the telemetry hook.

Response semantics (every /iiif/... response)

  • Cache + conditional GET. Each response carries Cache-Control and an ETag, and is written via http.ServeContent, so If-None-Match / If-Modified-Since yield 304.
  • CORS. Access-Control-Allow-Origin: * on every IIIF response.
  • Content negotiation. info.json honours Accept: application/ld+json (vs application/json), with the matching JSON-LD Content-Type.
  • Redirect precedence. Hook.Redirect is consulted at the top of the image and info handlers — before auth and parameter parsing — so a non-empty URL fires a 303 immediately (content relocation), distinct from an auth-conditional 303.

Test coverage

In internal/server/ (integration tests against an httptest server):

  • collapse_slashes_test.go — slash folding before routing.
  • content_neg_test.go — info.json Accept negotiation + JSON-LD type.
  • cache_integration_test.go — ETag / conditional-GET 304.
  • iplimit_test.go — the per-IP cap returns 429.
  • hook_redirect_test.go — Hook.Redirect fires a 303 before parsing/auth.
  • stats_test.go — per-route classification for the dashboard.
  • server_integration_test.go — end-to-end routing + status mapping.
  • source_error_test.go — a malformed identifier answers 400 with a fixed body on both the info and image routes, a missing one still 404.

Out of scope

  • IIIF parameter grammar + supported features — region/size/rotation/ quality/format coverage and the info.json field set live in iiif-compliance.md; the grammar itself is in iiif/v{2,3}.ParsePath.
  • The decode/render pipeline — image-decode.md.
  • Auth service request/response shapes — auth.md.
  • Admin UI internals (pages, SSE, config editor) — contract in admin.md; operator reference in ../admin.md.