gRPC Driver
The gRPC driver adapts generated Anvil gRPC services to
google.golang.org/grpc.
Install it:
go get github.com/TDB-Group/anvil-grpcImport it as:
import anvilgrpc "github.com/TDB-Group/anvil-grpc"Basic Use
Section titled “Basic Use”Native gRPC server options go directly to the driver:
app := anvil.New( anvilgrpc.Driver( grpc.MaxRecvMsgSize(16 << 20), ),)When installed with HTTP and GraphQL drivers, Anvil’s edge listener accepts the
connection and dispatches gRPC requests to this driver when the parsed request
is HTTP/2 and Content-Type starts with application/grpc.
The driver also exposes standalone Start, Serve, and ServeConn paths for
tests and low-level integrations. In a normal multi-protocol app, Anvil
serves the public listener and calls the gRPC driver through ServeHTTP.
Shutdown(ctx) starts grpc.Server.GracefulStop and waits for one of three
outcomes. If graceful stop finishes first, it returns nil. If ctx is canceled
first, the driver calls Stop and returns the context error. If
GracefulStopTimeout expires first, the driver calls Stop and returns nil.
Options
Section titled “Options”Use DriverWithOptions for the full configuration surface:
app := anvil.New( anvilgrpc.DriverWithOptions(anvilgrpc.Options{ ServerOptions: []grpc.ServerOption{ grpc.MaxRecvMsgSize(16 << 20), }, GracefulStopTimeout: 15 * time.Second, ServiceDescriptors: []protoreflect.ServiceDescriptor{projectDesc}, }),)Supported options:
| Option | Behavior |
|---|---|
ServerOptions | Native grpc.ServerOption values. |
ErrorPipeline | Error mapper and observer pipeline. Driver and DriverWithOptions fill this from app.ErrorPipeline() when the option is nil. |
GracefulStopTimeout | Time allowed for grpc.Server.GracefulStop before the driver calls Stop. Defaults to 10s. |
ServiceDescriptors | Protobuf service descriptors used for metadata validation. |
RequireServiceDescriptors | Fails registration when a generated service has no matching descriptor. |
Nil ErrorPipeline options are replaced with a default pipeline before
validation, or with the owning app pipeline when installed through Driver or
DriverWithOptions. The driver rejects a negative graceful stop timeout.
GracefulStopTimeout: 0 selects the driver default of 10s.
DriverFrom installs an existing transport and sets that transport’s error
pipeline to the owning app pipeline. Use
anvilgrpc.NewWithOptions(anvilgrpc.Options{ErrorPipeline: ...}) when the
transport is running standalone or when a test needs to inspect the transport
before it is installed.
Native grpc.ServerOption values stay native. Unary and stream interceptors
registered through gRPC server options run at the gRPC server layer. Anvil’s
generated middleware runs inside the registered service method after the gRPC
request has reached the Anvil service descriptor.
For unary calls, a gRPC unary interceptor receives a handler that runs the Anvil middleware chain and generated handler. For streaming calls, gRPC stream interceptors wrap the generated stream handler at the gRPC server layer. An interceptor can reject a call before Anvil middleware runs.
ServerOptions and ServiceDescriptors are copied when the transport is
created. Mutating the caller-owned slices after New, Driver, or
DriverWithOptions does not change the running driver.
Service Shape
Section titled “Service Shape”Application services embed sdk.GrpcEndpoint:
type ProjectService struct { sdk.GrpcEndpoint `service:"anvil.sample.ProjectService"`
Store project.Store `inject:"read"`}Unary handlers use:
func (s *ProjectService) GetProject( ctx context.Context, req *pb.GetProjectRequest,) (*pb.Project, error)Server streaming handlers use:
func (s *ProjectService) StreamProjects( ctx context.Context, req *pb.StreamProjectsRequest, stream sdk.GRPCServerStream[*pb.Project],) errorClient streaming and bidirectional streaming use the corresponding SDK stream contracts:
sdk.GRPCClientStream[*pb.Request]sdk.GRPCBidiStream[*pb.Request, *pb.Response]Generated code converts those typed stream contracts to the driver’s untyped
sdk.GRPCStream bridge.
Registration fails before the service is exposed when:
- The service name is empty.
- The service has no unary or streaming methods.
- The service name was already registered on the same driver.
- A unary or streaming method name is empty.
- Two generated methods on the same service use the same name.
- A unary method has no request factory or handler.
- A server-streaming method has no request factory.
- A streaming method has an invalid stream kind or no handler.
- Middleware on a method does not implement
sdk.GRPCMiddleware.
Middleware
Section titled “Middleware”gRPC middleware uses HandleGRPC and applies to unary and streaming methods:
func (RPCTrace) HandleGRPC(ctx sdk.GRPCCtx) (any, error) { method := ctx.FullMethod() kind := ctx.StreamKind() _ = method _ = kind
response, err := ctx.Next() if err != nil { return nil, err }
return response, nil}HandleGRPC is Anvil’s generated middleware method. It is not a native
grpc.UnaryServerInterceptor or grpc.StreamServerInterceptor. Native gRPC
interceptors still belong in grpc.ServerOption values passed to the driver;
Anvil middleware runs after the request reaches the generated service method.
Use gRPC middleware when the code needs Anvil’s generated service metadata: service name, method name, stream kind, decoded unary request, and generated dependency wiring. Use native gRPC interceptors when the code belongs at the raw gRPC server layer, before Anvil’s generated service method is entered.
Attach gRPC middleware to a parent group:
type V1 struct { sdk.Group `path:"/v1"` _ sdk.Use[middleware.RPCTrace]
ProjectService *rpc.ProjectService}Generated gRPC middleware is group-scoped. Put service-wide middleware on the nearest group that contains the service. The SDK has no per-method gRPC policy marker.
ctx.Next() can be called once. Calling it twice returns an internal
sdk.Failure.
HTTP middleware does not run for gRPC methods. Even though gRPC uses HTTP/2 on
the wire, Anvil dispatches it as gRPC and applies only sdk.GRPCMiddleware
components.
Use ctx.StreamKind() when middleware needs different behavior for unary,
server-streaming, client-streaming, or bidirectional streaming calls. Use
ctx.Stream() only for middleware that truly needs raw stream access. Normal
application handlers use the typed stream contracts generated for their method
signatures.
Unary methods report sdk.GRPCStreamUnknown from ctx.StreamKind(). Streaming
methods report sdk.GRPCStreamServer, sdk.GRPCStreamClient, or
sdk.GRPCStreamBidi.
For unary and server-streaming methods, ctx.Request() contains the decoded
request message. For client-streaming and bidirectional streaming methods,
ctx.Request() is nil and the middleware reads messages through ctx.Stream()
only when it intentionally needs raw stream access.
Middleware runs in inherited group order. The first middleware receives the
call, its ctx.Next() calls the next middleware, and the final continuation
calls the generated RPC handler. Returning without ctx.Next() stops the RPC
before the generated handler runs. That is useful for auth, deadline policy, or
load-shedding decisions.
Read Middleware for the full execution
model and sdk.GRPCCtx fields.
Stream Behavior
Section titled “Stream Behavior”The driver maps generated stream shapes onto gRPC as follows:
| Stream kind | Driver behavior |
|---|---|
| Unary | Allocates a request with NewRequest, decodes it, then sends the returned response. |
| Server streaming | Allocates and receives one request before calling the handler. The handler sends responses through the typed server stream. |
| Client streaming | Passes nil request and a receive-capable stream to the handler. The handler returns one response, and the driver sends it after the handler finishes. |
| Bidirectional streaming | Passes nil request and a send/receive stream to the handler. The handler controls both receive and send. |
If a request factory returns nil, or a client-streaming handler returns nil for its final response, the driver maps that as an internal Anvil failure.
For server-streaming methods, the driver receives one request message before it calls the handler. For client-streaming and bidirectional methods, the driver passes nil request data to the handler and exposes message receive through the stream contract.
Client-streaming handlers return one final response. After the handler returns,
the driver sends that response with stream.Send. Server-streaming and
bidirectional handlers send their own responses through the stream and the
driver ignores the returned any value.
Error Mapping
Section titled “Error Mapping”The driver maps Anvil failures to gRPC status codes:
HTTP status in sdk.Failure | gRPC code |
|---|---|
400, 422 | InvalidArgument |
401 | Unauthenticated |
403 | PermissionDenied |
404 | NotFound |
409 | AlreadyExists |
412 | FailedPrecondition |
429 | ResourceExhausted |
501 | Unimplemented |
503 | Unavailable |
504 | DeadlineExceeded |
| Any other status | Internal |
Decode errors become 400 Bad Request failures before they are mapped to gRPC.
Handler and middleware panics are recovered and mapped through the same error
pipeline as handler-phase errors.
Because recovered gRPC panics are published with handler phase, event.Recovered
is false for those events. The failure cause still contains the panic text and
the gRPC response is still an internal status unless a custom mapper changes
the classification. HTTP drivers use panic phase for recovered HTTP and
WebSocket panics; gRPC does not.
The status message is the mapped Anvil failure message. The driver publishes the mapped failure before returning the gRPC status error.
If the standalone gRPC server returns grpc.ErrServerStopped, the driver
normalizes that to nil. That keeps ordinary graceful stops from surfacing as
application errors.
Descriptor Validation
Section titled “Descriptor Validation”When a descriptor’s full service name matches a generated service name, the
driver validates generated service metadata against that descriptor before
registering the service. This catches schema drift between generated Anvil
metadata and .proto definitions.
Set RequireServiceDescriptors when production builds must fail if a service
is missing descriptor coverage.
Validation checks:
- The generated service must have a matching descriptor when descriptors are required.
- Descriptor entries cannot be nil, and descriptor names cannot be duplicated.
- Extra descriptors for services outside the Anvil route tree are ignored.
- Every generated unary method must exist on the descriptor.
- Every generated stream method must exist and match the generated stream kind.
- The descriptor method count must match the total generated unary and stream method count for that service.
Testbed Runner
Section titled “Testbed Runner”anvilgrpc.RunTestbed(ctx, suite, options) executes gRPC cases from an Anvil
testbed suite. The runner lives in the gRPC driver because it needs gRPC client
and protobuf descriptor support.
report, err := anvilgrpc.RunTestbed(ctx, suite, anvilgrpc.TestbedOptions{ Target: "127.0.0.1:8080", DialOptions: []grpc.DialOption{ grpc.WithTransportCredentials(insecure.NewCredentials()), }, Descriptors: []protoreflect.ServiceDescriptor{projectDescriptor},})TestbedOptions.Conn lets callers provide an existing client connection.
Target and DialOptions are used when Conn is nil. Descriptors are used
to map JSON-shaped testbed messages into protobuf messages. Timeout applies
per testbed call when it is positive. When it is zero or negative, the runner
uses the caller’s context without adding a deadline. When Conn is nil,
Target is required and the runner opens a grpc.ClientConn with
grpc.NewClient; that connection is closed by the runner. A caller-provided
Conn is not closed.
The testbed runner needs descriptors for the service and method under test. It
uses them to build dynamic protobuf messages from JSON-shaped case input and to
convert protobuf responses into JSON-shaped maps. Unary cases call
ClientConn.Invoke. Streaming cases use
ClientConn.NewStream with stream flags derived from the manifest kind, send
every item in input.messages or one input.message when messages is empty,
call CloseSend, then read responses until EOF. Expected messages are compared
as JSON-shaped maps after actual protobuf responses have been converted through
protojson.
The streaming runner is batch-oriented. It sends all configured input messages
first, closes the client send side, and then reads all responses. It does not
interleave send and receive steps inside one case. If expect.message and
expect.messages are both empty, the runner checks only the gRPC status code.