Responses
Handlers return the response body as a Go value.
func (p *Projects) Get(ctx sdk.Ctx, req GetProjectRequest) (ProjectDTO, error) { return p.Store.Find(ctx.Context(), req.ProjectID)}Response metadata lives on ctx.Response().
Status Codes
Section titled “Status Codes”ctx.Response().Status(http.StatusCreated)Anvil does not provide sdk.OK, sdk.Created, or similar helpers. Use the Go
standard library status constants or your own application constants.
Official HTTP drivers record an error when a handler sets a status outside the
100..999 range. That error is mapped through the normal error pipeline before
the response is written.
Headers
Section titled “Headers”ctx.Response().Header("Location", "/projects/"+created.ID.String())Headers stay explicit and under handler control.
An empty header name is recorded as a response error and mapped through the normal error pipeline.
Cookies
Section titled “Cookies”Use ctx.Response().Cookie(...) to set response cookies:
ctx.Response().Cookie(&http.Cookie{ Name: "session", Value: token, Path: "/", Expires: expiresAt, Secure: true, HttpOnly: true, SameSite: http.SameSiteLaxMode,})The official HTTP drivers append cookies as separate Set-Cookie headers.
Do not use ctx.Response().Header("Set-Cookie", cookie.String()); that shape
is easy to get wrong when a response sets more than one cookie.
Read request cookies through ctx.Request().Cookie("session"). For typed
request models, use a cookie binding:
type RefreshRequest struct { Session string `cookie:"session" validate:"required"`}Body Encoding
Section titled “Body Encoding”Drivers encode returned values through their codec registry. The standard Anvil
registry supports JSON and XML, with JSON as the default when the request does
not send Accept.
Anvil does not force every response through a generic response wrapper. If a route needs XML, NDJSON, CSV, or a custom content type, configure the driver and set the response metadata in the handler.
The driver’s codec registry selects the response codec from the request
Accept header. An empty Accept header and */* use the default codec. If no
registered codec can satisfy the header, the driver returns 406 Not Acceptable.
Negotiation is intentionally simple and deterministic:
- The header is split on commas.
- Media type parameters are stripped, so
application/json; charset=utf-8matchesapplication/json. - Entries are checked in header order.
- Quality weights such as
q=0.8are not sorted or reweighted.
Request body decoding uses the request Content-Type header. An empty
Content-Type uses the default codec. With the standard registry that means
JSON.
Error responses use the same response codec negotiation. The official HTTP drivers write this shape for mapped failures:
{ "error": { "status": 400, "message": "invalid request", "fields": { "name": "is required" } }}If an error response cannot be encoded, the drivers fall back to a plain
internal-server-error JSON body with status 500.
Empty Responses
Section titled “Empty Responses”Return nil when a route has no body:
func (p *Projects) Delete(ctx sdk.Ctx, req DeleteProjectRequest) (any, error) { if err := p.Store.Delete(ctx.Context(), req.ProjectID); err != nil { return nil, ctx.Errors().Wrap(err, "deleting project") }
ctx.Response().Status(http.StatusNoContent) return nil, nil}This keeps body, status, and metadata under application control.
Default behavior:
- Body returned and no status set:
200 OK. nilbody and no status set:204 No Content.nilbody and status set: the status you set, with no body.- Stream configured with
ctx.Response().Stream(...): the stream writes the response directly and the handler must returnnil.
A handler cannot return both a normal body and a stream. Official HTTP drivers reject that combination because the stream controls response framing.