Skip to content

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().

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.

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.

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"`
}

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-8 matches application/json.
  • Entries are checked in header order.
  • Quality weights such as q=0.8 are 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.

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.
  • nil body and no status set: 204 No Content.
  • nil body 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 return nil.

A handler cannot return both a normal body and a stream. Official HTTP drivers reject that combination because the stream controls response framing.