Skip to content

Middleware

Middleware is compiled from Go types marked with sdk.Use[T].

If you want the shortest explanation of which callback runs when, read Callbacks and Middleware first. The rest of this page describes how generated middleware chains are declared, checked, registered, and executed.

The compiler reads those marker fields, checks the method set of T, and emits one middleware chain per protocol. During a request, operation, RPC call, or queue delivery, the selected driver executes the generated chain with ordinary Go interface calls. There is no route lookup or middleware discovery through request-time reflection.

Middleware has four steps:

  • Declaration: application code places sdk.Use[T] on a group or policy type.
  • Compilation: cmd/anvil checks the method set of T with go/types.
  • Registration: generated code resolves concrete middleware values and attaches them to generated route metadata.
  • Execution: the selected driver receives that metadata and runs ordinary Go interface calls.

There is no global middleware registry. If a middleware type is not visible through sdk.Use[T], it is not part of the generated chain.

These rules explain the whole model:

  • Middleware types are ordinary Go structs. The compiler does not care about the type name; it cares where sdk.Use[T] appears and which protocol method T implements.
  • Implement the smallest method that matches the job.
  • sdk.Use[T] decides where a middleware type is attached.
  • Methods on T decide which protocol can run that middleware.
  • HTTP and WebSocket routes use HTTP middleware because a WebSocket starts as an HTTP upgrade request.
  • GraphQL uses GraphQL middleware, even though the driver receives the request through HTTP.
  • RPC calls use gRPC middleware, even though the wire protocol is HTTP/2.
  • Queue jobs use queue middleware after the broker message has been converted to sdk.QueueMessage.
  • Call ctx.Next() as the generated continuation for the current middleware invocation. Handlers return (body, error) instead of calling it.
  • HandleHTTP, HandleGraphQL, HandleGRPC, and HandleQueue are wrapper methods. They decide whether the generated chain continues by calling ctx.Next().
  • HTTP also has three focused methods for common HTTP-only jobs: BeforeHTTP, OnHTTPError, and AfterHTTP.

Use protocol-specific middleware types by default. A single type can implement multiple protocol methods for shared telemetry or tracing, but route files are easier to read when HTTPAuth, GraphQLAuth, RPCAuth, and QueueAudit are separate types.

When you see a sample with HTTPRequestTrace, GraphQLTrace, RPCTrace, and QueueTrace, that is intentional. They represent the same concern in four protocol shapes. Keeping them as separate types makes the route tree easier to audit, and it prevents one protocol’s context from leaking into another.

Middleware has two independent pieces:

PieceWritten AsMeaning
Placement_ sdk.Use[RequireActor]Put this middleware type into the generated chain at this group or policy boundary.
Protocol methodHandleHTTP, HandleGRPC, HandleGraphQL, HandleQueueTell the compiler which protocol chain can use the type.

That split is what makes mixed groups possible. A group can contain REST, WebSocket, GraphQL, gRPC, and queue jobs. The generated route metadata still keeps the middleware chains separate:

  • HTTP and WebSocket routes receive middleware values with HTTP middleware methods.
  • GraphQL endpoints receive middleware values with HandleGraphQL.
  • gRPC methods receive middleware values with HandleGRPC.
  • Queue jobs receive middleware values with HandleQueue.

A type with both BeforeHTTP and HandleGRPC can be attached once on a shared group. HTTP routes run the HTTP method and gRPC calls run the gRPC method. In normal application code, split those into separate types unless the same cross-cutting concern truly belongs to both protocols.

Application code writes three things:

  • A route tree with sdk.Group, controllers, GraphQL endpoints, gRPC services, and queue jobs.
  • Policy structs that contain _ sdk.Use[SomeMiddleware] fields.
  • Middleware types whose method names match the protocol they are meant to run on.

For HTTP, use route policies:

type ReadPolicy struct {
_ sdk.Use[middleware.RequireActor]
_ sdk.Use[middleware.AuditTrail]
}
type Routes struct {
Get sdk.GETWith[ReadPolicy] `path:"/:projectId"`
}

Policy structs can embed other policy structs anonymously. The compiler flattens embedded policy middleware at the point where the embedded field appears, then continues reading the rest of the policy fields in source order.

For GraphQL endpoint-specific middleware, use sdk.GraphQLEndpointWith[Policy]:

type ProjectGraph struct {
sdk.GraphQLEndpointWith[GraphPolicy] `path:"/graphql"`
}
type GraphPolicy struct {
_ sdk.Use[middleware.GraphQLAudit]
}

For gRPC and queue middleware, attach the middleware to a parent group:

type V1 struct {
sdk.Group `path:"/v1"`
_ sdk.Use[middleware.RPCTrace]
_ sdk.Use[middleware.QueueTrace]
ProjectService *rpc.ProjectService
Reindex *jobs.ReindexProjects
}

The compiler keeps those chains separate even when they share the same parent group.

Anvil middleware has two separate jobs:

  • sdk.Use[T] decides where middleware is attached.
  • The method on T decides which protocol can run it.

That is the whole contract. sdk.Use[RequireActor] does not mean “run this for every protocol.” It means “make this type available to the generated chain at this point in the route tree.” The compiler then checks the type’s methods and keeps the chain for each protocol separate.

HTTP has four method shapes because HTTP handlers return a simple body/error pair, while HTTP middleware often needs only one part of the request: setup, branching, error normalization, or final observation. GraphQL, gRPC, and queue each use one wrapper method because their response models are already protocol-specific.

The method names are part of the public SDK contract. Anvil uses method names instead of a global middleware registry so the compiler can validate the method set with go/types, generated code can carry concrete values, and drivers can execute the selected protocol without importing application packages.

This page uses “middleware method” for HandleHTTP, BeforeHTTP, HandleGraphQL, HandleGRPC, and HandleQueue. Plugin lifecycle hooks are a different concept. OnBoot and OnShutdown run while the application starts or stops; middleware methods run inside one request, GraphQL operation, RPC call, or queue delivery.

HandleHTTP is not an endpoint handler and it is not a Fiber/Gin-style callback. It means “this type wraps one generated HTTP middleware step.” HandleGRPC, HandleGraphQL, and HandleQueue mean the same thing for their protocols.

Anvil does not have an Around middleware method. A wrapper middleware is the Handle... method for its protocol, and the continuation is ctx.Next(). Using one continuation shape keeps generated code and drivers consistent while letting each protocol expose the context it actually needs.

The generated chain uses method presence as the opt-in. A type with only BeforeHTTP is valid HTTP middleware. A type with only AfterHTTP is valid HTTP middleware. A type with only HandleGRPC is valid gRPC middleware. Anvil does not require a base middleware interface with every method because that would force application code to write empty methods.

The compiler checks the method set with Go type information. It does not look for naming conventions such as AuthMiddleware, does not read comments, and does not register middleware globally. If a type is not attached through sdk.Use[T], it is not in the generated chain. If T does not implement the method required by the endpoint protocol, compilation fails with a diagnostic instead of dropping the middleware.

Middleware methods are named by protocol because the protocol determines what a middleware can safely see and return.

HTTP middleware works with sdk.Ctx, a response body, and an error. That shape fits normal REST handlers and WebSocket upgrades, so HTTP has separate methods for the common phases:

  • BeforeHTTP prepares request state before the next middleware runs.
  • HandleHTTP controls flow and decides whether to call ctx.Next().
  • OnHTTPError receives errors returned by downstream middleware or handlers.
  • AfterHTTP observes or changes the final body and error.

GraphQL, gRPC, and queue execution have different shapes from HTTP. GraphQL returns sdk.GraphQLResponse, gRPC calls can be unary or streaming, and queues have no response body. Those protocols use one wrapper method each:

  • HandleGraphQL
  • HandleGRPC
  • HandleQueue

Write setup code before ctx.Next(). Write cleanup, response shaping, audit, metrics, or error normalization after ctx.Next() returns. That is the interceptor model most Go developers already know from gRPC, but Anvil builds the chain from route metadata instead of asking you to register it by hand.

These methods are not application lifecycle hooks. Lifecycle hooks are plugin callbacks such as OnBoot and OnShutdown. Middleware methods run per request, per GraphQL operation, per RPC call, or per queue message.

Use one middleware type per protocol by default. A single type can implement more than one protocol method, but split types make the route tree easier to read.

type HTTPRequestTrace struct{}
func (HTTPRequestTrace) BeforeHTTP(ctx sdk.Ctx) error {
ctx.Response().Header("X-Request-ID", newRequestID())
return nil
}
type GraphQLTrace struct{}
func (GraphQLTrace) HandleGraphQL(ctx sdk.GraphQLCtx) (sdk.GraphQLResponse, error) {
return ctx.Next()
}
type RPCTrace struct{}
func (RPCTrace) HandleGRPC(ctx sdk.GRPCCtx) (any, error) {
return ctx.Next()
}
type QueueTrace struct{}
func (QueueTrace) HandleQueue(ctx sdk.QueueCtx) error {
return ctx.Next()
}

Those method names are middleware contracts, not plugins. A type with BeforeHTTP participates in the HTTP chain. A type with HandleGRPC participates in the gRPC chain. A type with both can be declared once on a shared group, and Anvil attaches it to both protocol-specific chains.

Attach them at a group when every endpoint in that group inherits them:

type V1 struct {
sdk.Group `path:"/v1"`
_ sdk.Use[middleware.HTTPRequestTrace]
_ sdk.Use[middleware.GraphQLTrace]
_ sdk.Use[middleware.RPCTrace]
_ sdk.Use[middleware.QueueTrace]
Projects *projects.Projects
ProjectGraph *graph.ProjectGraph
ProjectService *rpc.ProjectService
Reindex *jobs.ReindexProjects
}

In that example the group is mixed, but the chains stay separate. The HTTP middleware runs for HTTP and WebSocket routes. The GraphQL middleware runs for GraphQL endpoints. The gRPC middleware runs for gRPC methods. The queue middleware runs for queue jobs. Sharing a parent group does not make one protocol run another protocol’s middleware.

This means a mixed group is safe:

  • HTTP routes receive only middleware values with an HTTP method.
  • WebSocket routes receive the same HTTP middleware values because the upgrade starts as an HTTP request.
  • GraphQL endpoints receive only middleware values with HandleGraphQL.
  • Methods for gRPC receive only middleware values with HandleGRPC.
  • Queue jobs receive only middleware values with HandleQueue.

sdk.Use[T] is a marker. It does not create a field at runtime; it tells the compiler that type T belongs in a generated middleware chain.

Use it in these places:

LocationApplies ToExample
sdk.Group fieldsNested HTTP, GraphQL, gRPC, and queue endpoints that match the middleware protocolVersion tracing, shared auth, tenant loading
HTTP route policy typesOne HTTP or WebSocket routesdk.GETWith[ReadPolicy]
GraphQL endpoint policy typesOne GraphQL endpointsdk.GraphQLEndpointWith[GraphPolicy]

Controller structs are not middleware boundaries. The HTTP scanner reads route markers from the controller’s Routes struct, and it reads middleware from route policies or parent groups. Put controller-wide middleware on the nearest sdk.Group, or put route-specific middleware in the sdk.GETWith[T], sdk.POSTWith[T], and related policy marker.

Place sdk.Use[...] on a parent group for gRPC services and queue jobs. For GraphQL endpoint-specific middleware, use sdk.GraphQLEndpointWith[Policy]. The compiler reports ANVIL220 when middleware is placed directly on endpoint structs that need an explicit group or policy boundary.

GraphQL has endpoint-specific policies through sdk.GraphQLEndpointWith[T]. HTTP has route-specific policies through sdk.GETWith[T], sdk.POSTWith[T], and the other method markers. gRPC and queue middleware is group-scoped in the SDK; attach it to the nearest sdk.Group that represents the desired boundary.

That rule keeps mixed groups readable:

type V1 struct {
sdk.Group `path:"/v1"`
_ sdk.Use[middleware.HTTPRequestTrace]
_ sdk.Use[middleware.GraphQLTrace]
_ sdk.Use[middleware.RPCTrace]
_ sdk.Use[middleware.QueueTrace]
Projects *projects.Projects
ProjectGraph *graph.ProjectGraph
ProjectService *rpc.ProjectService
Reindex *jobs.ReindexProjects
}

The group lists shared concerns in one place. The compiler splits them by protocol before registration, so the HTTP driver never receives GraphQL-only middleware and the gRPC driver never receives HTTP-only middleware.

Within one group or policy, middleware follows source field order. Across the tree, middleware runs from outer group to inner group, then route or endpoint policy. The generated code resolves every middleware as a pointer component, so pointer receiver methods work and inject-tagged fields on the middleware type are wired through DI before traffic starts.

Use this placement rule when reading a route tree:

PlacementScope
_ sdk.Use[T] on an outer groupEvery matching protocol endpoint below that group
_ sdk.Use[T] on an inner groupMatching protocol endpoints below the inner group, after outer middleware
sdk.GETWith[Policy], sdk.POSTWith[Policy], and friendsOne HTTP or WebSocket route
sdk.GraphQLEndpointWith[Policy]One GraphQL endpoint

There is no controller-level middleware slot. A controller is where HTTP handlers and route tables live. A group is where shared route-tree behavior lives.

Anvil decides which protocol a middleware supports from its method signature. These method names are the contract. Generated registration passes concrete middleware values to the driver. Drivers also validate those values when routes are manually registered, so invalid middleware fails during startup instead of being ignored.

ProtocolMethodContext
HTTPBeforeHTTP(ctx sdk.Ctx) errorHTTP request, response metadata, locals, errors
HTTPHandleHTTP(ctx sdk.Ctx) (any, error)Full HTTP chain control through ctx.Next()
HTTPAfterHTTP(ctx sdk.Ctx, body any, err error) (any, error)Body and error after downstream returns
HTTPOnHTTPError(ctx sdk.Ctx, err error) errorError returned by downstream
GraphQLHandleGraphQL(ctx sdk.GraphQLCtx) (sdk.GraphQLResponse, error)GraphQL request, subscription stream, response, next handler
gRPCHandleGRPC(ctx sdk.GRPCCtx) (any, error)Service, method, stream kind, request, stream, next handler
QueueHandleQueue(ctx sdk.QueueCtx) errorQueue message, delivery context, next job handler

A middleware can implement more than one protocol method. That is useful for a shared telemetry plugin. For application code, prefer protocol-specific types unless the same concern really belongs everywhere.

When a type implements more than one protocol method, Anvil only uses the method that matches the endpoint being registered. A type with both BeforeHTTP and HandleGRPC can be attached to a shared group; HTTP routes run the HTTP method and gRPC methods run the gRPC method.

HTTP has four method shapes because an HTTP middleware often needs one narrow part of the request lifecycle: setup, control-flow, error normalization, or response observation. GraphQL, gRPC, and queue middleware use one wrapper method each. For those protocols, write code before ctx.Next() for pre-work and code after ctx.Next() for post-work.

ctx.Next() is the continuation for middleware. Calling it runs the next middleware value in the generated chain. When the chain is exhausted, it runs the handler.

Use ctx.Next() inside middleware only. HTTP handlers also receive sdk.Ctx because they need request, response, locals, and error helpers, but handlers return their response body and error instead of calling ctx.Next(). Outside a middleware invocation there is no continuation. Calling ctx.Next() there returns an internal failure.

Every protocol driver enforces the same rule: one middleware invocation can call ctx.Next() once. This keeps downstream handler side effects from being run twice.

If a middleware never calls ctx.Next(), the chain stops there. For HTTP and GraphQL that means the middleware returns the final body or response. For gRPC and queue jobs it means the middleware has handled the call itself and the actual service method or job handler will not run.

ctx.Next() is not a web-framework callback. It is the generated Anvil continuation for the current middleware invocation. Drivers install it before calling the middleware method and remove it afterward.

Use the value returned by ctx.Next() only in the protocol shape you are implementing:

MethodContinuation Result
HandleHTTPBody and error returned by the next HTTP middleware or handler
HandleGraphQLsdk.GraphQLResponse and error from the next GraphQL middleware or resolver
HandleGRPCRPC response and error from the next gRPC middleware or method
HandleQueueError from the next queue middleware or job

The HTTP-only methods BeforeHTTP, OnHTTPError, and AfterHTTP leave continuation to the driver. The driver handles continuation for BeforeHTTP, passes downstream errors to OnHTTPError, and passes the final body/error pair to AfterHTTP.

For an HTTP route, generated code runs:

  1. Group middleware inherited from outer to inner groups.
  2. Route policy middleware from sdk.GETWith[T], sdk.POSTWith[T], and the other HTTP method markers.
  3. Generated binding and validation.
  4. The handler method.
  5. Error and after methods while the chain unwinds.

The four method shapes are optional. Pick the smallest method that matches the job:

  • BeforeHTTP for setup before the route continues.
  • HandleHTTP when the middleware must decide whether to continue.
  • OnHTTPError when it only cares about downstream errors.
  • AfterHTTP when it needs to observe or change the final body/error pair.

For each middleware value in the chain, the HTTP driver runs methods in this order:

  1. BeforeHTTP, if the value implements it.
  2. HandleHTTP, if the value implements it. Otherwise the driver calls ctx.Next() for that value.
  3. OnHTTPError, if downstream returned an error and the value implements it.
  4. AfterHTTP, if the value implements it.

With two middleware values, the order looks like this when the handler returns success:

A.BeforeHTTP
A.HandleHTTP before ctx.Next()
B.BeforeHTTP
B.HandleHTTP before ctx.Next()
Handler
B.HandleHTTP after ctx.Next()
B.AfterHTTP
A.HandleHTTP after ctx.Next()
A.AfterHTTP

When the handler returns an error, error methods run as the call stack unwinds:

A.BeforeHTTP
A.HandleHTTP before ctx.Next()
B.BeforeHTTP
B.HandleHTTP before ctx.Next()
Handler returns error
B.OnHTTPError
B.AfterHTTP
A.OnHTTPError
A.AfterHTTP

AfterHTTP receives the body and error after that same middleware value’s OnHTTPError has run. Outer middleware receives whatever the inner chain returned.

If BeforeHTTP returns an error, that middleware value stops immediately. Outer middleware that already called into it still receives the error while the chain returns.

WebSocket routes use the same HTTP middleware chain. For a WebSocket route, ctx.Next() performs the upgrade and runs the socket handler. BeforeHTTP and the code before ctx.Next() run before the upgrade. OnHTTPError and AfterHTTP run when the upgrade attempt or socket handler returns.

Use BeforeHTTP when the middleware only needs to prepare request state and return an error to stop the request.

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
}

Good uses:

  • Add request IDs.
  • Load tenant metadata.
  • Perform cheap preflight checks.
  • Set response headers returned by every handler.

Use HandleHTTP when the middleware decides whether the request continues. Call ctx.Next() to run the next middleware or handler.

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 chain. That is useful for auth failures, cached responses, maintenance mode, and rate limits.

Use AfterHTTP when the middleware needs the body or error produced by downstream code.

func (AuditTrail) AfterHTTP(ctx sdk.Ctx, body any, err error) (any, error) {
audit.Record(ctx.Context(), ctx.Request().Path(), body, err)
return body, err
}

It can observe the result, replace the body, replace the error, or convert an error into a successful response by returning body, nil.

AfterHTTP receives the error after OnHTTPError has had a chance to normalize it. Use OnHTTPError for error mapping and AfterHTTP for final observation or response shaping.

Use OnHTTPError when the middleware only cares about downstream errors.

func (AuditTrail) 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. Returning an error keeps it in the pipeline.

GraphQL endpoints use sdk.GraphQLEndpoint or sdk.GraphQLEndpointWith[Policy].

type ProjectGraph struct {
sdk.GraphQLEndpointWith[GraphPolicy] `path:"/graphql"`
Store project.Store `inject:"read"`
}
type GraphPolicy struct {
_ sdk.Use[middleware.GraphQLAudit]
}

GraphQL middleware wraps Execute and Subscribe:

func (GraphQLAudit) HandleGraphQL(ctx sdk.GraphQLCtx) (sdk.GraphQLResponse, error) {
req := ctx.Request()
_ = req
response, err := ctx.Next()
if err != nil {
return response, err
}
if response.Extensions == nil {
response.Extensions = map[string]any{}
}
response.Extensions["audit"] = "ok"
return response, nil
}

Group middleware runs first, then the endpoint policy middleware, then the resolver. Group path prefixes still apply, so a graph endpoint under /api and /v1 with path:"/graphql" registers at /api/v1/graphql.

The call order is:

Outer group GraphQL middleware
Inner group GraphQL middleware
Endpoint policy GraphQL middleware
Execute or Subscribe

GraphQL uses its own middleware method because GraphQL responses contain Data, Errors, and Extensions. Treating it as normal HTTP middleware would hide GraphQL semantics and make subscription handling unclear.

GraphQL still shares the public HTTP-family listener with REST and gRPC. The edge dispatcher selects the GraphQL driver by registered GraphQL path before normal HTTP route matching.

GraphQL middleware has no separate BeforeGraphQL, AfterGraphQL, or OnGraphQLError methods. Put setup before ctx.Next(), inspect or change the sdk.GraphQLResponse after ctx.Next(), and return a different error when the middleware needs to normalize a failure.

If a GraphQL middleware panics, the GraphQL driver recovers the panic and returns it as an error. The error is then mapped by the GraphQL driver’s error handling path. Middleware still returns errors normally; panic recovery keeps a bad handler from taking down the process.

gRPC middleware attaches through groups and wraps unary and streaming methods.

type RPCTrace struct{}
func (RPCTrace) HandleGRPC(ctx sdk.GRPCCtx) (any, error) {
method := ctx.FullMethod()
kind := ctx.StreamKind()
_ = method
_ = kind
return ctx.Next()
}

sdk.GRPCCtx exposes:

  • Context() for cancellation, deadlines, and tracing.
  • Service() for the generated service name.
  • Method() for the method name.
  • FullMethod() for the canonical gRPC method path.
  • StreamKind() for unary, server-streaming, client-streaming, or bidirectional streaming.
  • Request() for unary and server-streaming request payloads.
  • Stream() for raw stream access when a middleware needs it.
  • Next() for the next middleware or RPC handler.

Use gRPC middleware for interceptor-style concerns such as auth, telemetry, deadline policy, stream accounting, and error mapping.

The call order is:

Outer group gRPC middleware
Inner group gRPC middleware
Unary method or streaming method

The gRPC driver handles panic recovery and converts the final error through its transport mapping. Middleware returns normal Go errors or sdk.Failure values; it does not write protocol frames directly.

gRPC middleware has one wrapper method for unary and streaming methods. Use ctx.StreamKind() to branch when a concern behaves differently for unary, server-streaming, client-streaming, or bidirectional streaming calls. Use ctx.Stream() only when middleware really needs raw stream access; normal application handlers use the generated typed stream interfaces.

Queue middleware attaches through groups and wraps queue job handlers.

type QueueTrace struct{}
func (QueueTrace) HandleQueue(ctx sdk.QueueCtx) error {
message := ctx.Message()
_ = message
return ctx.Next()
}

sdk.QueueCtx exposes the job context, the broker-neutral message, and Next(). Broker-specific delivery metadata stays in the queue driver.

Use queue middleware for job logging, retries, dead-letter metadata, tenant loading, and idempotency checks.

The call order is:

Outer group queue middleware
Inner group queue middleware
Queue job handler

Queue middleware runs inside the queue driver after a broker message has been converted to sdk.QueueMessage. Broker-specific ack, nack, visibility timeout, and delivery behavior stay in the driver.

Queue middleware has one wrapper method because queue jobs have no response body. Put delivery setup before ctx.Next() and retry or audit logic after ctx.Next() returns.

Queue drivers recover panics from queue middleware and handlers and return them as ordinary errors to the driver delivery path. Broker-specific retry and dead-letter behavior then follows the selected driver.

ctx.Locals() exists on sdk.Ctx, which is the HTTP and WebSocket request context. It is the right place for request actor, request ID, tenant ID, and policy decisions that later HTTP handlers bind or read.

GraphQL, gRPC, and queue middleware expose protocol-specific context. Use that context, the Go context.Context, or explicit injected dependencies for shared state.

Use locals for request state. Put application services on regular struct fields and let Anvil wire them.

NeedUse
Add an HTTP request IDBeforeHTTP
Authenticate an HTTP or WebSocket routeHandleHTTP or BeforeHTTP
Add audit data after an HTTP handler returnsAfterHTTP
Normalize HTTP route errorsOnHTTPError
Trace GraphQL operations or subscriptionsHandleGraphQL
Add gRPC interceptor behaviorHandleGRPC
Wrap queue job deliveryHandleQueue
Apply a concern to a whole API versionsdk.Use[T] on a sdk.Group
Apply a concern to one HTTP routesdk.GETWith[Policy], sdk.POSTWith[Policy], and friends
Apply a concern to one GraphQL endpointsdk.GraphQLEndpointWith[Policy]

The compiler validates middleware before code generation.

ANVIL211 means the middleware type exists, but its method signature does not match the protocol where it was used. Example: putting an HTTP-only middleware inside a GraphQL policy.

ANVIL220 means sdk.Use[...] was placed directly inside a GraphQL endpoint, gRPC service, or queue job. Move it to a parent sdk.Group, or use sdk.GraphQLEndpointWith[Policy] for GraphQL endpoint-specific middleware.

The diagnostic points at the source location that needs to change. Anvil does not silently drop middleware that it cannot apply.