Skip to content

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:

NeedMethod
Prepare request state and always continueBeforeHTTP
Decide whether the handler runsHandleHTTP
Normalize downstream errorsOnHTTPError
Observe or change the final body/error pairAfterHTTP

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.

The method names describe the part of the HTTP chain a middleware wants to own:

  • BeforeHTTP runs setup code and then Anvil continues automatically.
  • HandleHTTP controls the branch. It calls ctx.Next() when the request continues.
  • OnHTTPError sees only downstream errors and can replace or clear them.
  • AfterHTTP sees 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.

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.

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:

  1. Outer group middleware.
  2. Inner group middleware.
  3. Route policy middleware.
  4. Binding and validation.
  5. Handler.
  6. Error and after methods while returning back through the chain.

For each middleware value, methods run in this order:

  1. BeforeHTTP, if implemented.
  2. HandleHTTP, if implemented. Otherwise Anvil continues to the next item.
  3. OnHTTPError, if downstream returned an error and the value implements it.
  4. 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.BeforeHTTP
Outer.HandleHTTP before ctx.Next()
Inner.BeforeHTTP
Inner.HandleHTTP before ctx.Next()
Handler
Inner.HandleHTTP after ctx.Next()
Inner.AfterHTTP
Outer.HandleHTTP after ctx.Next()
Outer.AfterHTTP

If the handler returns an error, methods run while the stack returns:

Handler returns error
Inner.OnHTTPError
Inner.AfterHTTP
Outer.OnHTTPError
Outer.AfterHTTP

For 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 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
}

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
}

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.

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.

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.

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.