GraphQL Driver
The GraphQL driver adapts generated GraphQL endpoints to GraphQL-over-HTTP.
Install it:
go get github.com/TDB-Group/anvil-graphqlImport it as:
import anvilgraphql "github.com/TDB-Group/anvil-graphql"Basic Use
Section titled “Basic Use”app := anvil.New( anvilgraphql.Driver(),)When installed with HTTP and gRPC drivers, Anvil’s edge listener selects the GraphQL driver by exact request path. The driver then decodes the GraphQL HTTP payload and runs the generated endpoint.
The driver also has standalone Start and ServeConn paths for tests and
low-level integrations. In a normal multi-protocol app, Anvil runs the
public listener and calls this driver as an http.Handler.
Options
Section titled “Options”app := anvil.New( anvilgraphql.Driver(anvilgraphql.Options{ MaxBodyBytes: 8 << 20, MuxPath: "/graphql", }),)Supported options:
| Option | Behavior |
|---|---|
Server | Cloned *http.Server settings for standalone driver use. Handler must be nil. |
ErrorPipeline | Error mapper and observer pipeline. Driver fills this from app.ErrorPipeline() when the option is nil. |
MaxBodyBytes | Maximum POST body size. 0 selects the driver default of 4 MiB; negative values are rejected. |
MuxPath | Path reported to Anvil’s edge before generated routes are registered. Defaults to /graphql. Once generated routes are registered, the driver reports the sorted unique set of registered GraphQL route paths instead. |
Nil ErrorPipeline options are replaced with a default pipeline before
validation, or with the owning app pipeline when installed through Driver.
The driver rejects negative timeouts, negative body limits, a server with a
pre-filled Handler, and more than one options value.
DriverFrom installs an existing transport and sets that transport’s error
pipeline to the owning app pipeline. Use
anvilgraphql.New(anvilgraphql.Options{ErrorPipeline: ...}) when the
transport is running standalone or when a test needs to inspect the transport
before it is installed.
MuxPath is trimmed. An empty value becomes /graphql; a non-empty value
without a leading slash receives one.
MaxBodyBytes uses http.MaxBytesReader for POST bodies. The option cannot
disable the default limit by setting 0; 0 means “use the default.”
Server is for standalone driver use. In a normal Anvil app, public listener
settings come from anvil.WithListener because the edge runtime serves the
shared HTTP-family listener.
When a custom Server leaves timeout fields empty, the driver applies these
defaults to the cloned server:
| Server setting | Default |
|---|---|
ReadHeaderTimeout | 5s |
ReadTimeout | 30s |
WriteTimeout | 30s |
IdleTimeout | 2m |
The standalone GraphQL driver does not have its own TLS option. Use Anvil’s
ListenTLS/RunTLS for normal applications, or terminate TLS before the
standalone server.
Endpoint Shape
Section titled “Endpoint Shape”GraphQL endpoints embed sdk.GraphQLEndpoint or
sdk.GraphQLEndpointWith[Policy]:
type ProjectGraph struct { sdk.GraphQLEndpointWith[GraphPolicy] `path:"/graphql"`
Store project.Store `inject:"read"`}Resolvers implement Execute:
func (g *ProjectGraph) Execute( ctx context.Context, req sdk.GraphQLRequest,) (sdk.GraphQLResponse, error)Subscriptions are optional:
func (g *ProjectGraph) Subscribe( ctx context.Context, req sdk.GraphQLRequest, stream sdk.GraphQLSubscriptionStream,) errorAn endpoint path is still part of the group tree. If a GraphQL endpoint is
nested under /api and /v1, path:"/graphql" becomes /api/v1/graphql.
The driver matches the final request path exactly. It does not inspect the GraphQL body during edge classification and it does not perform route fallback after a path is selected.
Registration fails when the path is empty, the handler is nil, the middleware
value does not implement sdk.GraphQLMiddleware, or another GraphQL route has
already registered the same path.
RegisterGraphQL stores route.Path exactly as supplied. It does not trim
spaces, add a leading slash, lowercase, or clean trailing slashes. Generated
Anvil code passes final compiler paths. The helper anvilgraphql.Route(...)
normalizes its own path argument before it creates an sdk.GraphQLRoute.
Executor Helpers
Section titled “Executor Helpers”The driver exposes a small adapter contract for schema engines:
type Executor interface { Execute(context.Context, sdk.GraphQLRequest) (sdk.GraphQLResponse, error)}
type Subscriber interface { Subscribe(context.Context, sdk.GraphQLRequest, sdk.GraphQLSubscriptionStream) error}anvilgraphql.Route(path, endpoint, executor) converts an executor into
sdk.GraphQLRoute. If the executor also implements Subscriber, the generated
route gets a subscription handler too.
Route trims the path, converts an empty path to /graphql, and adds a
leading slash when the path has none. It rejects a nil executor and an empty
endpoint name.
ExecutorFunc and SubscriberFunc adapt plain functions. They return errors
when called on nil functions, so nil wiring fails as an ordinary Go error.
HTTP Request Shape
Section titled “HTTP Request Shape”The driver accepts GET and POST.
GET reads:
queryoperationNamevariables, decoded as a JSON object ornullwhen presentextensions, decoded as a JSON object ornullwhen present
POST reads JSON:
{ "query": "query Project($id: ID!) { project(id: $id) { id name } }", "operationName": "Project", "variables": {"id": "proj_123"}, "extensions": {}}query is required for both methods. variables and extensions must decode
to JSON objects or null when present. Unsupported methods return only
405 Method Not Allowed; the driver does not build a GraphQL error body or
publish an Anvil error event for that method check.
POST decodes the request body as JSON and enforces MaxBodyBytes with
http.MaxBytesReader. Invalid JSON, missing body, invalid query parameters,
and missing query are mapped as 400 Bad Request GraphQL errors with public
message invalid graphql request.
The POST decoder reads one JSON request object into query, operationName,
variables, and extensions. Extra fields are ignored by the JSON decoder.
The driver does not require a specific Content-Type before decoding a POST
body. The body still has to be valid GraphQL JSON.
Response Shape
Section titled “Response Shape”Responses are encoded as GraphQL HTTP JSON:
{ "data": {}, "errors": [], "extensions": {}}GraphQL handler errors are mapped through Anvil’s error pipeline. The driver
returns a GraphQL error response with the mapped HTTP status and also stores the
status in errors[].extensions.status.
The GraphQL driver always writes application/json for normal GraphQL
responses. It does not negotiate response codecs through Accept; Accept is
only used to select SSE subscriptions.
If JSON encoding of the GraphQL response fails, the driver writes HTTP 500
with a fixed GraphQL error body. That bypasses the normal mapper because the
response encoder is already the failing component.
Panics from GraphQL execution, subscription handlers, and GraphQL middleware are recovered and mapped as handler-phase errors.
Subscriptions
Section titled “Subscriptions”When the route has a subscription handler and any request Accept header value
contains the case-sensitive literal substring text/event-stream, the driver
uses server-sent events. It does not parse Accept quality values for this
decision.
Subscriptions use SSE. The GraphQL driver does not implement WebSocket-based GraphQL subscriptions.
For normal query/mutation execution, ctx.Subscription() is nil. For SSE
subscription execution, ctx.Subscription() is the stream passed to the
subscription handler.
Subscription events are emitted as:
nextfor payloads sent throughstream.Senderrorwhen a mapped error happens after the stream startscompletewhen the subscription finishes after starting
When the stream starts, the driver writes HTTP 200 with
Content-Type: text/event-stream, sets Cache-Control: no-cache, sets
Connection: keep-alive, disables proxy buffering with X-Accel-Buffering: no, and flushes the response. Each stream.Send payload is encoded through
the same GraphQL HTTP JSON response shape used by normal queries.
If the response writer does not support flushing, subscriptions fail with a
503 Anvil failure.
The driver starts the SSE stream immediately before it calls the generated
Subscribe handler. Middleware that fails before calling ctx.Next() can still
produce a normal GraphQL error response. Errors returned by Subscribe, or by
middleware after ctx.Next() has started the stream, are emitted as error
events and then the stream completes.
If the route has no subscription handler, an Accept: text/event-stream
request still runs the normal Execute handler.
Middleware
Section titled “Middleware”GraphQL middleware uses HandleGraphQL:
func (GraphQLTrace) 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["trace"] = "enabled" return response, nil}HandleGraphQL is the only GraphQL middleware method. It wraps the generated
Execute and Subscribe calls and works with sdk.GraphQLRequest and
sdk.GraphQLResponse, not with raw HTTP response bodies. Put setup before
ctx.Next() and response or error work after ctx.Next() returns.
Use GraphQL middleware when the code needs GraphQL operation data, response extensions, or subscription stream state. Use HTTP middleware for REST and WebSocket routes. GraphQL arrives over HTTP, but Anvil classifies it by the registered GraphQL path and sends it to the GraphQL driver before normal HTTP route matching.
Attach shared GraphQL middleware to a parent group:
type V1 struct { sdk.Group `path:"/v1"` _ sdk.Use[middleware.GraphQLTrace]
ProjectGraph *graph.ProjectGraph}Attach endpoint-specific GraphQL middleware through
sdk.GraphQLEndpointWith[Policy]:
type GraphPolicy struct { _ sdk.Use[middleware.GraphQLAudit]}That policy type is the GraphQL equivalent of an HTTP route policy such as
sdk.GETWith[ReadPolicy]. Keep endpoint-specific middleware in the policy
type, not as anonymous sdk.Use[...] fields on the endpoint struct.
Place sdk.Use[...] on a group or policy type. The compiler rejects middleware
fields directly inside a GraphQL endpoint struct so the route tree stays clear.
GraphQL does not use HTTP middleware even though GraphQL is served over HTTP. The GraphQL context exposes GraphQL request data, subscription stream access, and a GraphQL response shape. Return a mapped error when a GraphQL-specific failure needs normalization.
Middleware runs in inherited group order, then endpoint policy order. The first
middleware receives the operation, its ctx.Next() calls the next middleware,
and the final continuation calls Execute or Subscribe. Returning without
ctx.Next() stops the resolver. That is useful for auth, persisted-query
checks, operation limits, or cached GraphQL responses.
ctx.Next() can be called once by one middleware invocation. Calling it twice,
or calling it when no middleware continuation is installed, returns an internal
Anvil failure.
GraphQL middleware and resolver panics are recovered as handler-phase errors.
That means observers receive a mapped failure, but event.Recovered is false
because the final phase is handler, not panic. The public GraphQL response
still contains the mapped error and extensions.status.
Read Middleware for execution order,
sdk.GraphQLCtx, and compiler diagnostics.