Table of Contents
Protocol and Wire Format
TrevRPC uses a custom protobuf protocol over native QUIC, ordinary HTTP/3, and WebTransport. It is intentionally small: one length-prefixed protobuf control message to start a call, protobuf payload bytes for application messages, and status codes to terminate calls.
Compatibility
TrevRPC is not wire-compatible with Connect, gRPC, or gRPC-Web.
ConnectRPC can speak Connect, gRPC, and gRPC-Web protocols. TrevRPC speaks its own protocol even when carried by ordinary HTTP/3. The similarity is at the developer workflow level: protobuf schema, generated clients, generated server bindings, and typed handlers.
Native QUIC ALPN
The current ALPN value is:
trevrpc/1
Rust exports it as trevrpc::ALPN, a byte string. Go exports it as trevrpc.ALPN, and Kotlin exports it as zip.trev.trevrpc.ALPN; both are strings.
Applications using native QUIC must configure TLS and QUIC with this ALPN value. The runtime does not hide certificate or mTLS setup.
WebTransport Negotiation
WebTransport uses HTTP/3 WebTransport negotiation, not ALPN trevrpc/1. Once a WebTransport session is established, every RPC still uses the same TrevRPC frame encoding described below.
Ordinary HTTP/3 Mapping
Ordinary HTTP/3 uses ALPN h3 and one full-duplex POST /trevrpc request per RPC. Both request and response use Content-Type: application/trevrpc. HTTP/3 DATA payloads contain exactly the TrevRPC frames described below; DATA boundaries are independent of TrevRPC frame boundaries.
Servers send HTTP 200 after HTTP-level admission. RPC failures continue to use RpcResponse or terminal RpcStreamFrame status values. HTTP trailers are not part of the mapping. See HTTP/3 for configuration and pre-RPC HTTP status handling.
Frame Encoding
Each TrevRPC frame is:
4-byte big-endian body length
protobuf-encoded frame body
The frame length is an unsigned 32-bit integer. It is the protobuf body length only and does not include the 4-byte header.
The default maximum frame size is 4 MiB. Oversized frames are rejected and mapped to ResourceExhausted where applicable.
Wire Version
The current wire version is 1.
Every RpcRequest carries the version. Servers reject unsupported versions with FailedPrecondition.
Request Message
RpcRequest contains:
| Tag | Field | Type | Purpose |
|---|---|---|---|
1 |
service |
string |
Fully-qualified protobuf service name, such as example.greeter.Greeter |
2 |
method |
string |
Protobuf method name, such as SayHello |
3 |
body |
bytes |
Encoded unary request body or initial server-streaming request body |
4 |
metadata |
map<string, bytes> |
Request metadata map |
5 |
kind |
RpcKind |
RPC kind enum |
6 |
version |
uint32 |
TrevRPC wire version |
7 |
timeout_nanos |
uint64 |
Relative RPC timeout in nanoseconds, or 0 when unset |
RPC kinds:
| Value | Kind |
|---|---|
0 |
Unary |
1 |
Client streaming |
2 |
Server streaming |
3 |
Bidirectional streaming |
Unary Response Message
RpcResponse contains:
| Tag | Field | Type | Purpose |
|---|---|---|---|
1 |
status |
uint32 |
Numeric status code |
2 |
message |
string |
Status message |
3 |
body |
bytes |
Encoded protobuf response body |
4 |
metadata |
map<string, bytes> |
Response metadata map |
For non-OK statuses, clients return the status as an error instead of decoding the body.
Streaming Frame Message
RpcStreamFrame contains:
| Tag | Field | Type | Purpose |
|---|---|---|---|
1 |
kind |
RpcStreamFrameKind |
Message frame or status frame |
2 |
status |
uint32 |
Numeric status code for status frames |
3 |
message |
string |
Status message for status frames |
4 |
body |
bytes |
Encoded protobuf message body for message frames |
5 |
metadata |
map<string, bytes> |
Metadata map for status frames or future extensions |
Frame kinds:
| Value | Kind |
|---|---|
0 |
Message |
1 |
Status |
Streaming responses always end with a status frame. Streaming requests may end directly at transport EOF or with a terminal status frame. OK means graceful request completion; a non-OK request status propagates that status to the server-side request stream. After a request status, the sender must finish its transport send side and must not send another frame; receivers may process the status before transport EOF or drain through EOF to validate the terminal boundary.
Unknown RpcKind and RpcStreamFrameKind values are protocol errors. Servers reject unsupported request kinds with InvalidArgument; stream readers reject unknown stream-frame kinds with InvalidArgument or Internal depending on whether the invalid frame came from a peer request stream or response stream.
Metadata
Metadata is a map from lowercase ASCII string keys to arbitrary byte values.
Limits:
| Limit | Value |
|---|---|
| Entries | 64 |
| Key length | 128 bytes |
| Value length | 8 KiB |
| Total size | 64 KiB |
Key rules:
- Keys are normalized to lowercase by call option helpers.
- Keys must use lowercase ASCII letters, digits,
.,_, or-. - Empty keys are invalid.
- The
trevrpc-prefix is reserved for protocol use.
Metadata is validated by clients before sending and by servers before route lookup.
Metadata map ordering is not semantically meaningful. Implementations must not depend on map entry order.
Status Codes
TrevRPC uses canonical status codes with numeric values matching gRPC:
| Code | Value |
|---|---|
Ok |
0 |
Cancelled |
1 |
Unknown |
2 |
InvalidArgument |
3 |
DeadlineExceeded |
4 |
NotFound |
5 |
AlreadyExists |
6 |
PermissionDenied |
7 |
ResourceExhausted |
8 |
FailedPrecondition |
9 |
Aborted |
10 |
OutOfRange |
11 |
Unimplemented |
12 |
Internal |
13 |
Unavailable |
14 |
DataLoss |
15 |
Unauthenticated |
16 |
Unknown numeric status codes map to Unknown.
Timeouts
Clients can set per-call timeouts. The runtime sends the relative timeout in RpcRequest.timeout_nanos and also enforces the local timeout.
timeout_nanos = 0 means no wire timeout. Non-zero values are relative durations measured from the server receiving the request. Servers derive request context deadlines from that duration. Locally exceeded deadlines map to DeadlineExceeded.
Implementations must reject timeout values that cannot be represented by their local deadline type instead of wrapping or truncating them.
Golden Vectors
Shared golden vectors live in testdata/wire-golden-vectors.txt. C, Go, JavaScript, Kotlin, and Rust runtime tests consume this fixture to lock protobuf body bytes and length-prefixed frame bytes for representative requests, responses, metadata, timeouts, message frames, and status frames.
The fixture format is intentionally simple:
name.body = hex-encoded protobuf body
name.frame = hex-encoded 4-byte big-endian length prefix followed by the same protobuf body
Compatibility Policy
Wire compatibility is defined by protobuf field numbers, protobuf wire types, enum numeric values, frame length encoding, and the semantics documented here.
Compatible changes may add optional fields with new tag numbers or add new status codes that old peers safely map to Unknown. Incompatible changes include reusing tags, changing existing field types, changing existing enum values, changing frame length encoding, changing timeout_nanos semantics, or requiring peers to understand a new field to preserve existing behavior.
Incompatible changes require a new wire version. Rolling upgrades should introduce additive changes first, deploy readers that tolerate the new fields, and only then deploy writers that depend on them.
Transport Stream Usage
Native QUIC transports use one bidirectional QUIC stream per RPC. WebTransport transports use one bidirectional WebTransport stream per RPC. Ordinary HTTP/3 uses one full-duplex request stream per RPC. Transport-level unidirectional streams are not used for RPC data, although HTTP/3 requires control and QPACK streams.
Rust Quinn configuration selects trevrpc::quinn::TransportMode::Native to disable unused inbound unidirectional streams, or TransportMode::WebTransport for HTTP/3 and WebTransport endpoints. The Go and C server configuration helpers likewise allow HTTP/3 control streams only when an HTTP/3 transport is enabled.
Kotlin's Netty transport maps native QUIC, ordinary HTTP/3, and server-side WebTransport streams to the same core wire frames. NettyRpcServer uses one UDP listener and dispatches by negotiated ALPN and, for h3, by the accepted HTTP/3 request or WebTransport session.