ADR 0053 — HEAD is an explicit contract operation
- Status: Accepted — extends the raw-response boundary in ADR 0038 and file semantics in ADR 0023
- Date: 2026-08-07
Context
serveFile already implements HEAD, ranges and conditional requests, but the
contract method union could not declare HEAD. File and link-preview operations
therefore needed a raw route, losing contract identity, params, lifecycle/RBAC,
typed-client and OpenAPI coverage.
Treating GET as an automatic HEAD alias would hide a second operation from the
contract and make its authorization, observability and Allow semantics
implicit. A typed JSON output is also the wrong result model: HEAD has no
response body, while its useful result is status plus headers.
Decision
Add explicit method: 'HEAD' endpoints. They are HTTP-only raw-response
operations and must declare rawResponse: true. They may declare path params
and response contentType, but cannot declare input/output schemas, multipart,
raw-body retention or tool metadata/exposure.
GET and HEAD on the same path remain distinct router entries. A declared GET does not match HEAD, and vice versa. Method mismatch responses list only the operations actually declared for that path.
The handler receives the original Request and returns a Response, allowing
serveFile and custom handlers to derive conditional/range status and headers.
After the handler returns, Stitchkit constructs a bodyless response preserving
status, status text and headers. This makes the wire invariant hold even if a
custom handler accidentally supplied a body.
The built-in HTTP client exposes head; a typed HEAD contract method resolves
to the untouched Response. OpenAPI publishes the HEAD operation and a
bodyless success response.
Alternatives rejected
- Implicit GET → HEAD fallback. It invents an undeclared operation and
obscures lifecycle, authorization and
Allowbehavior. - Raw route with
method: 'ALL'. It bypasses the contract-owned lifecycle and repeats route identity outside the contract. - Typed empty output. It hides the only useful result—status and headers—and suggests JSON parsing where no body exists.
- Permit request input. Query data remains available through
req.url; a declared input would imply transport and validation semantics that HEAD does not need.
Consequences
- File and metadata probes can be fully contract-owned.
- Custom
HttpClientadapters add aheadprimitive. - HEAD remains deliberately unavailable to MCP, agent and CLI transports.
- Raw-response
afterHandlebehavior remains unchanged: it is skipped because there is no typed data result to transform.