Skip to content

RPC Call Lifecycle

Understanding the end-to-end lifecycle of a call helps you reason about instrumentation, performance optimisations, and where to plug in your own infrastructure pieces. This guide walks through every stage of a unary call and highlights how streaming requests build on the same pipeline.

Contract lookup

Caller resolves the contract metadata and chooses a transport route.

Transport handshake

A stream ID is reserved and request metadata is pushed through the transport.

Responder dispatch

The responder endpoint decodes payloads, finds a handler, and executes it.

Response & trailers

Results flow back with status trailers, health metrics, and optional context data.

  1. RpcCallerEndpoint receives a method invocation such as callUnary<Req, Res>() from a generated or handwritten caller class.
  2. The endpoint resolves the service name from the contract and adds routing headers (for example x-route-service) to the outgoing RpcContext.
  3. A transport stream ID is reserved via IRpcTransport.createStream(). Clients use odd IDs, responders use even IDs, as enforced by RpcStreamIdManager.
  4. Request metadata is emitted first. RPC Dart generates the canonical :method, :path, content-type, and te: trailers headers using RpcMetadata.forClientRequest().

RPC Dart supports three data transfer modes controlled by the responder contract:

  • Zero-copy transfers (default when no codecs are provided) push the original Dart object straight into RpcTransportMessage.directPayload. This is limited to transports that return supportsZeroCopy == true, such as RpcInMemoryTransport.
  • Codec transfers serialise payloads using user supplied IRpcCodec instances. The built-in RpcCodec helper uses CBOR for compact binary data.
  • Auto mode selects zero-copy when no codecs are supplied and falls back to codecs otherwise. This allows you to enable serialisation per-method.

When serialisation is required, RPC Dart wraps your payload with a gRPC-style 5-byte prefix (RpcMessageFrame.encode) and emits it through IRpcTransport.sendMessage().

  1. The responder endpoint listens on IRpcTransport.incomingMessages and groups frames by stream ID.
  2. Initial metadata is parsed to recover the method path and route the call to a registered RpcResponderContract.
  3. The contract resolves the registration created via addUnaryMethod (or a streaming equivalent) and decides whether zero-copy is allowed for the specific request/response pair.
  4. Payloads are either handed to the codec for deserialisation or delivered directly to the handler. The handler signature always receives the request object plus an optional RpcContext that carries headers, deadlines, and cancellation tokens.
  • For unary handlers RPC Dart awaits the returned future, serialises or forwards the response object, and sends it back via the original stream ID.
  • For streaming handlers the runtime creates a stream processor that merges payload messages with end-of-stream markers (RpcMessage.isEndOfStream). Bidirectional streams use the same infrastructure in both directions.
  • When the handler completes, the endpoint appends a trailer metadata frame with the final gRPC status (usually RpcStatus.OK).
  1. The caller endpoint observes the trailer, converts it into an RpcMessage<T> with isEndOfStream = true, and resolves the pending future or completes the response stream.
  2. If the trailer carries a non-zero status the endpoint throws an RpcError-like exception populated with the status code and message for easy debugging.
  3. Stream IDs are released back to the transport via releaseStreamId so that future calls can reuse them without reopening the connection.

Handlers return Stream<T> instead of Future<T>. The endpoint listens to the stream and converts each event into RpcTransportMessage frames. The caller receives a Dart stream that mirrors the server output one-to-one.

  • Insert middleware by implementing IRpcMiddleware and registering it on the endpoint. Middleware can inspect and mutate requests or responses before they reach handlers.
  • Observe diagnostics by calling RpcCallerEndpoint.health() or RpcResponderEndpoint.health(), which aggregate transport status and endpoint metrics into RpcEndpointHealth snapshots.
  • Custom transports hook into stages 2 and 5. Implement IRpcTransport (or derive from RpcBaseTransport) to connect RPC Dart to any messaging fabric without altering caller/responder code.

Understanding these steps makes it easier to decide where to add logging, metrics, retries, or protocol extensions while keeping application logic clean and portable.