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/anvilchecks the method set ofTwithgo/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.
Read This First
Section titled “Read This First”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 methodTimplements. - Implement the smallest method that matches the job.
sdk.Use[T]decides where a middleware type is attached.- Methods on
Tdecide 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, andHandleQueueare wrapper methods. They decide whether the generated chain continues by callingctx.Next().- HTTP also has three focused methods for common HTTP-only jobs:
BeforeHTTP,OnHTTPError, andAfterHTTP.
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.
Mental Model
Section titled “Mental Model”Middleware has two independent pieces:
| Piece | Written As | Meaning |
|---|---|---|
| Placement | _ sdk.Use[RequireActor] | Put this middleware type into the generated chain at this group or policy boundary. |
| Protocol method | HandleHTTP, HandleGRPC, HandleGraphQL, HandleQueue | Tell 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.
What You Write
Section titled “What You Write”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.
Contract
Section titled “Contract”Anvil middleware has two separate jobs:
sdk.Use[T]decides where middleware is attached.- The method on
Tdecides 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.
Why These Methods Exist
Section titled “Why These Methods Exist”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:
BeforeHTTPprepares request state before the next middleware runs.HandleHTTPcontrols flow and decides whether to callctx.Next().OnHTTPErrorreceives errors returned by downstream middleware or handlers.AfterHTTPobserves 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:
HandleGraphQLHandleGRPCHandleQueue
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.
Practical Rule
Section titled “Practical Rule”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.
Where sdk.Use Is Legal
Section titled “Where sdk.Use Is Legal”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:
| Location | Applies To | Example |
|---|---|---|
sdk.Group fields | Nested HTTP, GraphQL, gRPC, and queue endpoints that match the middleware protocol | Version tracing, shared auth, tenant loading |
| HTTP route policy types | One HTTP or WebSocket route | sdk.GETWith[ReadPolicy] |
| GraphQL endpoint policy types | One GraphQL endpoint | sdk.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:
| Placement | Scope |
|---|---|
_ sdk.Use[T] on an outer group | Every matching protocol endpoint below that group |
_ sdk.Use[T] on an inner group | Matching protocol endpoints below the inner group, after outer middleware |
sdk.GETWith[Policy], sdk.POSTWith[Policy], and friends | One 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.
Protocol Signatures
Section titled “Protocol Signatures”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.
| Protocol | Method | Context |
|---|---|---|
| HTTP | BeforeHTTP(ctx sdk.Ctx) error | HTTP request, response metadata, locals, errors |
| HTTP | HandleHTTP(ctx sdk.Ctx) (any, error) | Full HTTP chain control through ctx.Next() |
| HTTP | AfterHTTP(ctx sdk.Ctx, body any, err error) (any, error) | Body and error after downstream returns |
| HTTP | OnHTTPError(ctx sdk.Ctx, err error) error | Error returned by downstream |
| GraphQL | HandleGraphQL(ctx sdk.GraphQLCtx) (sdk.GraphQLResponse, error) | GraphQL request, subscription stream, response, next handler |
| gRPC | HandleGRPC(ctx sdk.GRPCCtx) (any, error) | Service, method, stream kind, request, stream, next handler |
| Queue | HandleQueue(ctx sdk.QueueCtx) error | Queue 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()
Section titled “ctx.Next()”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:
| Method | Continuation Result |
|---|---|
HandleHTTP | Body and error returned by the next HTTP middleware or handler |
HandleGraphQL | sdk.GraphQLResponse and error from the next GraphQL middleware or resolver |
HandleGRPC | RPC response and error from the next gRPC middleware or method |
HandleQueue | Error 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.
HTTP Execution
Section titled “HTTP Execution”For an HTTP route, generated code runs:
- Group middleware inherited from outer to inner groups.
- Route policy middleware from
sdk.GETWith[T],sdk.POSTWith[T], and the other HTTP method markers. - Generated binding and validation.
- The handler method.
- Error and after methods while the chain unwinds.
The four method shapes are optional. Pick the smallest method that matches the job:
BeforeHTTPfor setup before the route continues.HandleHTTPwhen the middleware must decide whether to continue.OnHTTPErrorwhen it only cares about downstream errors.AfterHTTPwhen 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:
BeforeHTTP, if the value implements it.HandleHTTP, if the value implements it. Otherwise the driver callsctx.Next()for that value.OnHTTPError, if downstream returned an error and the value implements it.AfterHTTP, if the value implements it.
With two middleware values, the order looks like this when the handler returns success:
A.BeforeHTTPA.HandleHTTP before ctx.Next() B.BeforeHTTP B.HandleHTTP before ctx.Next() Handler B.HandleHTTP after ctx.Next() B.AfterHTTPA.HandleHTTP after ctx.Next()A.AfterHTTPWhen the handler returns an error, error methods run as the call stack unwinds:
A.BeforeHTTPA.HandleHTTP before ctx.Next() B.BeforeHTTP B.HandleHTTP before ctx.Next() Handler returns error B.OnHTTPError B.AfterHTTPA.OnHTTPErrorA.AfterHTTPAfterHTTP 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.
BeforeHTTP
Section titled “BeforeHTTP”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.
HandleHTTP
Section titled “HandleHTTP”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.
AfterHTTP
Section titled “AfterHTTP”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.
OnHTTPError
Section titled “OnHTTPError”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 Execution
Section titled “GraphQL Execution”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 SubscribeGraphQL 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 Execution
Section titled “gRPC Execution”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 methodThe 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 Execution
Section titled “Queue Execution”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 handlerQueue 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.
Locals and State
Section titled “Locals and State”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.
When To Use What
Section titled “When To Use What”| Need | Use |
|---|---|
| Add an HTTP request ID | BeforeHTTP |
| Authenticate an HTTP or WebSocket route | HandleHTTP or BeforeHTTP |
| Add audit data after an HTTP handler returns | AfterHTTP |
| Normalize HTTP route errors | OnHTTPError |
| Trace GraphQL operations or subscriptions | HandleGraphQL |
| Add gRPC interceptor behavior | HandleGRPC |
| Wrap queue job delivery | HandleQueue |
| Apply a concern to a whole API version | sdk.Use[T] on a sdk.Group |
| Apply a concern to one HTTP route | sdk.GETWith[Policy], sdk.POSTWith[Policy], and friends |
| Apply a concern to one GraphQL endpoint | sdk.GraphQLEndpointWith[Policy] |
Compiler Diagnostics
Section titled “Compiler Diagnostics”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.