HTTP Middleware
HTTP middleware is compiled from sdk.Use[T] markers on groups and HTTP route
policies. It runs for normal HTTP routes and WebSocket upgrade routes.
HTTP is the only protocol with four middleware methods. That is because HTTP has a request, response metadata, a returned body, and a mapped error path. Use the smallest method that matches the job:
| Need | Method |
|---|---|
| Prepare request state and always continue | BeforeHTTP |
| Decide whether the handler runs | HandleHTTP |
| Normalize downstream errors | OnHTTPError |
| Observe or change the final body/error pair | AfterHTTP |
Anvil does not pass a separate next function into HTTP middleware. The
continuation lives on sdk.Ctx as ctx.Next() so the handler signature stays
the same shape as every other Anvil HTTP context user.
You normally implement one method per middleware type. Implement more than one
only when the same value needs both phases, such as an audit middleware that
normalizes errors with OnHTTPError and records the final result with
AfterHTTP.
For the full protocol middleware model, read Middleware.
HTTP middleware is not a plugin hook and it is not a framework callback from
Fiber, Gin, or net/http. The compiler emits route metadata, the selected HTTP
driver receives that metadata, and the driver calls these SDK methods through
ordinary Go interfaces.
What The Method Names Mean
Section titled “What The Method Names Mean”The method names describe the part of the HTTP chain a middleware wants to own:
BeforeHTTPruns setup code and then Anvil continues automatically.HandleHTTPcontrols the branch. It callsctx.Next()when the request continues.OnHTTPErrorsees only downstream errors and can replace or clear them.AfterHTTPsees the final body and error for observation or response shaping.
These are ordinary Go methods checked by the compiler. There is no runtime registration step beyond the generated route metadata.
HandleHTTP is the method to use when the middleware decides whether the next
middleware or handler runs. BeforeHTTP, OnHTTPError, and AfterHTTP cover
setup, error normalization, and result observation without requiring a full
wrapper method.
If a middleware type implements no HTTP middleware method, it cannot run on an HTTP or WebSocket route. If it is attached to an HTTP policy anyway, the compiler reports a middleware diagnostic before code generation completes.
Route Policies
Section titled “Route Policies”Use route policies when one route needs middleware that does not apply to the whole controller or group.
type WritePolicy struct { _ sdk.Use[RequireActor] _ sdk.Use[RequireWritePlan] _ sdk.Use[AuditTrail]}
type Routes struct { Create sdk.POSTWith[WritePolicy] `path:"/"`}Policy types keep route tables compact while making middleware composition a regular Go type.
Policy structs can embed other policy structs anonymously. Embedded policy middleware is flattened where the embedded field appears, so source field order is the execution order inside the policy.
Group Middleware
Section titled “Group Middleware”Use group middleware when every nested endpoint inherits the same behavior.
type V1 struct { sdk.Group `path:"/v1"` _ sdk.Use[HTTPRequestTrace]
Projects *projects.Projects}HTTP middleware attaches through groups and route policies. Put shared
controller middleware on the nearest sdk.Group, or attach it to each route
through a policy marker such as sdk.GETWith[Policy].
The generated order is deterministic:
- Outer group middleware.
- Inner group middleware.
- Route policy middleware.
- Binding and validation.
- Handler.
- Error and after methods while returning back through the chain.
For each middleware value, methods run in this order:
BeforeHTTP, if implemented.HandleHTTP, if implemented. Otherwise Anvil continues to the next item.OnHTTPError, if downstream returned an error and the value implements it.AfterHTTP, if implemented.
When a middleware implements multiple HTTP methods, those methods belong to the
same value. For example, if AuditTrail implements both OnHTTPError and
AfterHTTP, AuditTrail.OnHTTPError runs first for errors returned by the
downstream chain, then AuditTrail.AfterHTTP receives the possibly changed
error.
If BeforeHTTP returns an error, the same middleware value stops there. Outer
middleware can still observe the error as the chain returns.
ctx.Next() can be called once by one middleware invocation. Calling it again
returns an internal failure. Calling it outside middleware also returns an
internal failure because no continuation is installed there.
ctx.Next() is not part of normal handler code. HTTP handlers receive
sdk.Ctx for request data, response headers, locals, and errors; they finish by
returning (body, error). Middleware is the place that calls ctx.Next().
For HTTP, the body returned by ctx.Next() is the body the next middleware or
handler returned. Middleware can return that body unchanged, replace it, or
return an error. The driver writes the final body after the chain has returned
and after response metadata from ctx.Response() has been applied.
The successful path with two middleware values is:
Outer.BeforeHTTPOuter.HandleHTTP before ctx.Next() Inner.BeforeHTTP Inner.HandleHTTP before ctx.Next() Handler Inner.HandleHTTP after ctx.Next() Inner.AfterHTTPOuter.HandleHTTP after ctx.Next()Outer.AfterHTTPIf the handler returns an error, methods run while the stack returns:
Handler returns errorInner.OnHTTPErrorInner.AfterHTTPOuter.OnHTTPErrorOuter.AfterHTTPFor a WebSocket route, the same chain wraps the upgrade. ctx.Next() upgrades
the connection and runs the socket handler. Code before ctx.Next() runs while
the request is still an HTTP request; AfterHTTP and OnHTTPError run after
the upgrade attempt or socket handler returns.
If a WebSocket middleware returns before calling ctx.Next(), the connection
is not upgraded. The driver writes the returned body or mapped error as a normal
HTTP response.
HTTP Method Shapes
Section titled “HTTP Method Shapes”HTTP supports four middleware method shapes. These are not application lifecycle hooks. They are per-request method contracts that the generated HTTP chain checks and executes.
type HTTPMiddleware interface { HandleHTTP(ctx sdk.Ctx) (any, error)}
type HTTPBeforeMiddleware interface { BeforeHTTP(ctx sdk.Ctx) error}
type HTTPAfterMiddleware interface { AfterHTTP(ctx sdk.Ctx, body any, err error) (any, error)}
type HTTPErrorMiddleware interface { OnHTTPError(ctx sdk.Ctx, err error) error}BeforeHTTP
Section titled “BeforeHTTP”Use BeforeHTTP for setup that either succeeds or stops the request with an
error.
func (HTTPRequestTrace) BeforeHTTP(ctx sdk.Ctx) error { requestID := ctx.Request().Header("X-Request-ID") if requestID == "" { requestID = newRequestID() }
ctx.Locals().Set("requestID", requestID) ctx.Response().Header("X-Request-ID", requestID) return nil}HandleHTTP
Section titled “HandleHTTP”Use HandleHTTP when the middleware decides whether the chain continues.
func (RequireActor) HandleHTTP(ctx sdk.Ctx) (any, error) { token := ctx.Request().Header("Authorization") if token == "" { return nil, ctx.Errors().Failure(http.StatusUnauthorized, "missing authorization") }
actor, err := parseActor(token) if err != nil { return nil, ctx.Errors().Failure(http.StatusUnauthorized, "invalid authorization") }
ctx.Locals().Set("actor", actor) return ctx.Next()}Returning without calling ctx.Next() short-circuits the route.
Use this shape when the middleware controls the branch in the control flow: auth, rate limits, cache hits, maintenance mode, or any case where the handler does not always run.
AfterHTTP
Section titled “AfterHTTP”Use AfterHTTP for response audit, instrumentation, or response shaping.
func (AuditTrail) AfterHTTP(ctx sdk.Ctx, body any, err error) (any, error) { audit.Record(ctx.Context(), ctx.Request().Path(), body, err) return body, err}AfterHTTP receives the error after OnHTTPError has run. It can return the
same body and error, replace either value, or return body, nil to mark the
request as handled.
AfterHTTP does not run on the same middleware value when that value’s own
BeforeHTTP method returned an error. It does run for outer middleware values
that already called into the failing part of the chain.
OnHTTPError
Section titled “OnHTTPError”Use OnHTTPError to normalize errors from downstream middleware or handlers.
func (ErrorNormalizer) OnHTTPError(ctx sdk.Ctx, err error) error { if domain.IsRateLimited(err) { return ctx.Errors().Failure(http.StatusServiceUnavailable, "try again later") } return err}Returning nil marks the error as handled.
Use this shape when the middleware does not need to see successful responses.
If the middleware needs both successful and failed results, use AfterHTTP.
Locals
Section titled “Locals”ctx.Locals() carries request-scoped values between HTTP middleware and
handlers:
actor, _ := ctx.Locals().Get("actor").(Actor)Use locals for request actor, trace IDs, correlation IDs, and policy decisions. Use injected struct fields for application services.