flare_mlake · API · grpc

flare.grpc.server

flare.grpc.server -- gRPC server adapter on the HTTP/2 reactor.

A gRPC unary call maps onto a single HTTP/2 stream:

  • Request HEADERS: :method=POST, :scheme=http(s), :path=/<service>/<method>, content-type=application/grpc (or one of the +proto / +json flavours), te: trailers, plus optional grpc-encoding / grpc-accept-encoding / grpc-timeout / application metadata.
  • Request DATA: zero or more length-prefix-message (LPM) frames containing the serialized request bodies.
  • Response HEADERS: :status=200, content-type=application/ grpc, optional grpc-encoding.
  • Response DATA: zero or more LPM frames carrying the serialized reply bodies.
  • Response trailing HEADERS: grpc-status (REQUIRED), optional grpc-message, optional trailing metadata.

This module exposes the adapter that performs the validation + LPM stitching + status-code mapping, so a unary handler reads the decoded request bytes and writes either Ok(response_bytes) or a typed :class:GrpcStatus error and the adapter takes care of the rest.

Public surface:

  • :class:GrpcCallContext -- per-call view: full path, deadline (parsed from grpc-timeout), initial metadata, accept- encoding hint.
  • :class:GrpcUnary -- the user handler trait. serve_unary(ctx, request_bytes) -> (response_bytes, status, trailing_metadata).
  • :class:GrpcServerAdapter -- bridges one HTTP/2 stream to one :class:GrpcUnary call. Steps:
    1. validate request HEADERS;
    2. accumulate request DATA bytes until END_STREAM;
    3. decode LPM frames -> request bytes;
    4. invoke the unary handler;
    5. encode response bytes as LPM frame;
    6. emit response HEADERS + DATA + trailing HEADERS.

The adapter is sans-I/O at the codec boundary: byte streams in, HTTP/2 wire bytes (HEADERS frame field section + DATA payload + trailing HEADERS field section) out. Networking + flow control is the Http2Connection's job.

References:

  • gRPC PROTOCOL-HTTP2 spec.
  • RFC 9113 (HTTP/2) §4 (frames) + §6.10 (TRAILERS marker).

Structs

GrpcRequestHeaders

struct GrpcRequestHeaders

Typed carrier for the H2 request-HEADERS field set the gRPC adapter consumes.

The method / path / content_type / te fields are REQUIRED on every well-formed gRPC call; the optional fields (timeout -- grpc-timeout, accept_encoding -- grpc-accept-encoding) carry the empty :class:Optional when the client omits them. initial_metadata is the application-visible subset (grpc- reserved keys stripped).

The H2 driver builds this carrier from the parsed H2RequestHeaders field set and hands it to :func:parse_request_headers or :func:run_unary_call. The typed shape collapses the prior seven-positional-string signature into a single argument and lets the driver build each field with the right Optional[String] semantics.

Implements Copyable, Movable

Fields
method: String
path: String
content_type: String
te: String
timeout: Optional[String]
accept_encoding: Optional[String]
encoding: Optional[String]
initial_metadata: GrpcMetadata

GrpcCallContext

struct GrpcCallContext

View of the per-call HTTP/2 request state visible to the application handler.

The path field is the gRPC method name (/svc/method); the deadline is the absolute microsecond timestamp at which the server should abandon the call (0 means "no deadline specified"). Initial metadata is the application-visible subset of the request HEADERS (grpc- reserved keys are stripped before the handler sees them).

Implements Copyable, Movable

Fields
path: String
deadline_us: UInt64
initial_metadata: GrpcMetadata
accept_encoding: String

GrpcUnaryReply

struct GrpcUnaryReply

Typed return value for a unary gRPC handler.

Replaces the prior Tuple[List[UInt8], GrpcStatus, GrpcMetadata] so a handler's call site reads as return GrpcUnaryReply.ok(body) or return GrpcUnaryReply.err(GrpcStatus.err(...)) instead of a positional 3-tuple. The :func:run_unary_call driver always emits an empty response body for non-OK replies; body is ignored in that case (the spec carries the status on the trailers, not the payload).

The two factory methods (:func:ok and :func:err) handle the metadata-default ergonomics so a handler that does not attach trailing metadata can omit the argument.

Implements Copyable, Movable

Fields
body: List[UInt8]
status: GrpcStatus
trailing_metadata: GrpcMetadata

ok

def ok(var body: List[UInt8], var trailing_metadata: GrpcMetadata = GrpcMetadata()) -> Self

Build an OK reply carrying body and the (optional) trailing metadata. GrpcStatus.ok() is filled in for the caller.

err

def err(var status: GrpcStatus, var trailing_metadata: GrpcMetadata = GrpcMetadata()) -> Self

Build a non-OK reply carrying status plus optional trailing metadata. The body is always empty; the trailers carry the status code + message.

GrpcCallOutcome

struct GrpcCallOutcome

Output of one full unary call -- the bytes the H2 driver queues onto the response stream.

  • response_data is the encoded LPM frame for the response body (empty on non-OK status; the trailers carry the status).
  • status is the gRPC status code the trailer must carry.
  • trailing_metadata is the trailing HEADERS field set excluding grpc-status / grpc-message (those are framework-managed; see :func:emit_trailing_headers).

Implements Copyable, Movable

Fields
response_data: List[UInt8]
status: GrpcStatus
trailing_metadata: GrpcMetadata
encoding: String

GrpcService

struct GrpcService[H: GrpcUnary & Copyable & Deinitable]

Adapt a :class:GrpcUnary handler into a plain :trait:Handler so it serves over the unified :class:flare.http.HttpServer H2 reactor path -- no bespoke per-RPC glue.

Mount it like any other handler::

var svc = GrpcService(EchoHandler())
var server = HttpServer.bind(addr)
server.serve(svc)   # over h2c / h2

One H2 stream maps to one unary call: the reactor hands this adapter the assembled :class:Request (HEADERS + DATA), the adapter runs :func:run_unary_call, and returns a :class:Response whose trailers carry grpc-status. The inner handler is copied per call (H: Copyable) so the immutable serve borrow holds while serve_unary mutates its own copy -- unary handlers are config- only in practice, so the copy is cheap.

Deadlines: when the client sends grpc-timeout the adapter enforces it as a wall-clock ceiling on the handler invocation and returns DEADLINE_EXCEEDED if the handler overruns.

Deadline enforcement is a post-hoc elapsed check, not a cooperative mid-call cancel (the handler runs to completion, then we compare elapsed vs budget), so a runaway handler still burns its full time before the status flips. Cancelling it promptly would need a :class:flare.http.Cancel threaded into a cancel-aware GrpcUnary variant and flipped from a deadline timer.

Implements Copyable, Handler, Movable

Fields
handler: H
comptime BodyType = InlineBody

The Body impl this handler authors its response with.

serve

def serve(self, req: Request) -> Response

Traits

GrpcUnary

Single-method gRPC handler.

The handler receives the decoded request bytes (already LPM-stitched + decompressed) and returns a typed :class:GrpcUnaryReply carrying the response body + status + trailing metadata. Returning a non-OK reply causes the adapter to emit an empty response body + the trailers; the response bytes are ignored in that case.

Implements Movable

serve_unary

def serve_unary(mut self, ctx: GrpcCallContext, request_bytes: Span[UInt8]) -> GrpcUnaryReply

Functions

parse_request_headers

def parse_request_headers(headers: GrpcRequestHeaders) -> GrpcCallContext

Validate the H2 request HEADERS for a gRPC call and build the :class:GrpcCallContext.

Validation surface (all RFC + gRPC PROTOCOL-HTTP2 MUSTs):

  • :method MUST be POST.
  • :path MUST start with / and contain at least one additional / segment (/<service>/<method>).
  • content-type MUST be application/grpc[+flavor].
  • te MUST contain the trailers token. Without it the client wouldn't read the trailing HEADERS, so the call has no way to learn the status; the spec promotes the missing te: trailers to a connection-level fail-closed.

The optional grpc-timeout is parsed into a microsecond deadline; missing / unparseable timeouts surface as 0 (no deadline) so partial deployments don't fail closed.

stitch_request_data

def stitch_request_data(request_data: Span[UInt8], encoding: String = "", max_decompressed_bytes: Int = Int(16777216)) -> List[UInt8]

Stitch one or more LPM frames out of the accumulated request DATA bytes into a single contiguous payload.

Returns the concatenated payloads (in order). gRPC unary calls SHOULD only emit a single LPM frame, but the spec permits a server to receive multiple back-to-back frames inside one call (a streaming client packing requests through a unary stub is the example shape); the adapter handles both.

Compressed frames (flag bit 0 set) are decompressed using the call's grpc-encoding (gzip / deflate). A compressed frame with no / unsupported grpc-encoding raises.

max_decompressed_bytes bounds the whole call, not each frame: it was a per-frame cap, so a body of many small compressed frames, each inflating to just under 16 MiB, stitched into one payload of any size.

Raises on:

  • truncated LPM frame (need-more-data at the end of the request body),
  • compressed flag set with no negotiated / supported encoding,
  • decompressed output past max_decompressed_bytes.

encode_unary_response

def encode_unary_response(response_bytes: List[UInt8], mut out: List[UInt8], encoding: String = "")

Append a single LPM frame wrapping response_bytes to out.

When encoding is gzip / deflate and the payload is at least :data:GRPC_COMPRESS_MIN_BYTES, the payload is compressed and the frame's flag byte is set (bit 0). Otherwise an uncompressed frame (flag 0) is emitted. The caller is responsible for advertising the chosen codec on the response grpc-encoding header (see :func:_response_from_outcome).

The caller owns the buffer and may reuse the same List[UInt8] across responses so the underlying allocation amortises across the stream. The encoder appends only; it never reads from or truncates the existing contents.

emit_trailing_headers_status

def emit_trailing_headers_status(status: GrpcStatus) -> List[Tuple[String, String]]

Return the framework-controlled trailer entries for a completed gRPC call.

Always emits grpc-status (the wire-level integer outcome). Emits grpc-message when status.message is non-empty (clients use it for diagnostics only). Emits grpc-status-details-bin when status.details carries a payload, RFC 4648 §4 base64-encoded per gRPC PROTOCOL-HTTP2.

The caller (the H2 trailer encoder) appends the application trailing metadata after these framework-owned entries; this helper does not look at :class:GrpcMetadata.

Returning List[Tuple[String, String]] keeps the trailer emitter sans-I/O: the H2 driver decides how to serialise the pairs onto a HEADERS frame (HPACK literal-without-indexing, QPACK, h3 SETTINGS-aware, etc.).

run_unary_call

def run_unary_call[H: GrpcUnary](mut handler: H, headers: GrpcRequestHeaders, request_data: Span[UInt8]) -> GrpcCallOutcome

End-to-end driver: validate headers, stitch LPM, invoke the user handler, encode the response.

The HTTP/2 driver calls this once per stream; success produces a :class:GrpcCallOutcome whose bytes go on the DATA frame and whose status / trailing metadata go on the trailing HEADERS frame.

The driver itself never raises -- every failure mode (HEADERS validation, truncated/compressed LPM, handler raising) is folded into a typed :class:GrpcCallOutcome with the right grpc-status:

  • HEADERS validation failure -> GRPC_STATUS_INVALID_ARGUMENT (the gRPC spec maps client malformed input to this code).
  • LPM stitch failure (truncated frame, compressed without a negotiated encoding) -> GRPC_STATUS_INVALID_ARGUMENT.
  • Handler raises -> GRPC_STATUS_INTERNAL with the fixed message "handler raised". The raise text stays on the server: the failure is a server-side bug, not client input, and the text can carry anything the exception held.

Aliases

comptime CONTENT_TYPE_GRPC = String("application/grpc")

Canonical gRPC content-type. The adapter also accepts the flavored variants application/grpc+proto / +json / +thrift -- any string starting with this prefix and an optional +<flavor> segment.

comptime GRPC_COMPRESS_MIN_BYTES = Int(64)

Response payloads at or above this size are eligible for message-level compression when the client advertises a codec via grpc-accept-encoding. Below it the gzip/deflate header overhead outweighs the saving, so the frame ships uncompressed (flag 0). This is a fixed cutoff, not an adaptive ratio probe.