HTTP/3
TrevRPC servers can accept ordinary HTTP/3 requests in addition to native QUIC and WebTransport. This transport is useful for clients such as Android Cronet that expose HTTP/3 request streams but not arbitrary QUIC streams or WebTransport sessions.
Kotlin provides ordinary HTTP/3 clients through both Netty on the JVM and an application-injected Cronet engine on Android. Other language clients remain transport-specific.
Wire Mapping
Each RPC uses one full-duplex HTTP/3 request stream:
| HTTP field | Value |
|---|---|
| Method | POST |
| Default path | /trevrpc |
| Content-Type | application/trevrpc |
| Response code | 200 after acceptance |
| Response type | application/trevrpc |
The request DATA payload is the existing TrevRPC byte stream: one length-prefixed RpcRequest, followed by request RpcStreamFrame values when the RPC accepts a client stream. The response DATA payload is the existing length-prefixed RpcResponse or response RpcStreamFrame sequence.
HTTP/3 DATA frame boundaries have no TrevRPC meaning. Implementations accept TrevRPC frames split across DATA frames or multiple TrevRPC frames coalesced into one DATA frame.
The media type is ASCII case-insensitive. Parameters, duplicate values, and comma-combined values are rejected.
Status Handling
Servers use HTTP status codes only before accepting an RPC:
| Condition | HTTP status |
|---|---|
| Unknown or disabled path | 404 |
Method other than POST |
405 |
| Invalid Content-Type | 415 |
| Admission callback rejection | 403 |
| Per-connection stream limit | 503 |
After sending 200, the normal TrevRPC status rules apply. Unary failures use RpcResponse; streaming calls terminate with an RpcStreamFrame status. HTTP trailers are not used.
Go Server
Enable HTTP/3 and advertise h3 alongside native TrevRPC when both transports share a listener:
options := server.Options()
options.EnableHTTP3 = true
options.HTTP3Path = "/trevrpc"
server.SetOptions(options)
tlsConfig.NextProtos = []string{trevrpc.ALPN, http3.NextProtoH3}
listener, err := quic.ListenAddr(
"127.0.0.1:50051",
tlsConfig,
trevrpc.QUICServerConfig(server.Options(), nil),
)
if err != nil {
return err
}
return trevrpc.ServeQUIC(ctx, listener, server)
HTTP3Admission is optional. When set, it receives the request, method, path, authority, and TLS state after method, path, and media-type validation but before TrevRPC framing is read.
Go dispatches ordinary POST and WebTransport CONNECT requests through the same http3.Server, so both can use one h3 connection and listener.
Rust Server
The default http3 feature provides ordinary HTTP/3 serving through h3 and h3-quinn. The webtransport feature adds unified WebTransport handling through h3-webtransport.
let options = trevrpc::server::ServerOptions::new()
.with_http3_enabled(true)
.with_http3_path("/trevrpc");
server.set_options(options);
server_crypto.alpn_protocols = vec![
trevrpc::ALPN.to_vec(),
trevrpc::HTTP3_ALPN.to_vec(),
];
server
.serve_quinn_and_webtransport_with_shutdown(endpoint, shutdown)
.await?;
Use serve_http3 or serve_http3_with_shutdown for an HTTP/3-only endpoint. Http3AdmissionRequest exposes path, authority, TLS state, and a transport-neutral regular-header view.
The unified server supports one WebTransport session per HTTP/3 connection. Ordinary HTTP/3 requests can continue while that session is active.
C Server
The C runtime uses the existing MsQuic listener and its bounded HTTP/3/QPACK implementation:
trevrpc_server_config config = trevrpc_default_server_config();
config.host = "127.0.0.1";
config.port = 50051;
config.cert_file = "server-cert.pem";
config.key_file = "server-key.pem";
config.enable_http3 = 1;
config.http3_path = "/trevrpc";
trevrpc_server* server = NULL;
int rc = trevrpc_server_listen(&config, &server);
Set http3_admission for an optional synchronous admission callback. HTTP/3 request-header parsing and RPC handling use the bounded server worker pool, so one partial request stream does not block other streams on the connection.
The C HTTP/3 implementation advertises zero QPACK dynamic-table capacity, supports the complete static table and literal/Huffman fields, bounds field sections, incrementally consumes DATA, and treats malformed H3/QPACK connection state as connection-scoped errors.
Node Server
Node uses the C/MsQuic server and exposes the same transport:
const server = await NodeServer.listen("https://127.0.0.1:50051", {
certFile: "server-cert.pem",
keyFile: "server-key.pem",
enableHttp3: true,
http3Path: "/trevrpc",
http3Admission: ({ secure }) => secure,
});
http3Admission must return synchronously. Its native wait is bounded by initialRequestTimeoutMs, which defaults to 10 seconds.
Kotlin Netty Server
NettyRpcServer accepts native QUIC, ordinary HTTP/3, and WebTransport on one UDP listener. Enable HTTP/3 and optionally install admission after method, path, and media-type validation:
val listener = NettyRpcServer.bind(
server,
NettyRpcServerConfig(
bindAddress = InetSocketAddress("127.0.0.1", 50051),
tls = NettyServerTls.Pem(keyFile, certificateFile),
enableNative = true,
enableHttp3 = true,
http3Admission = Http3Admission { request ->
request.secure && request.authority == "localhost:50051"
},
),
)
The fixed mapping is POST /trevrpc with application/trevrpc. Ordinary POST requests remain available while an admitted WebTransport session is active on the same H3 connection.
Kotlin Clients
On the JVM, create the routine long-lived RpcChannel for HTTP/3:
val config = NettyQuicClientConfig(
remoteAddress = InetSocketAddress("127.0.0.1", 50051),
tls = NettyClientTls("localhost", trustCertificates = listOf(certificate)),
)
val channel: RpcChannel = NettyRpcChannel.http3(config)
channel.awaitReady()
val client = GreeterClient(channel)
The channel reconnects for future calls. In-flight RPCs remain on their original generation and fail if it dies; calls made during reconnection fail immediately with Unavailable. zip.trev.trevrpc.netty.advanced.RawNettyHttp3RpcTransport is the advanced single-connection API for integrations, benchmarks, diagnostics, and deterministic tests.
On Android, CronetRpcChannel.create adapts CronetEngine.newBidirectionalStreamBuilder to the same RpcChannel interface:
val channel: RpcChannel = CronetRpcChannel.create(
engine = cronetEngine,
origin = "https://api.example.com",
callbackExecutor = cronetExecutor,
)
The application selects and owns the Cronet provider, engine, callback executor, device trust policy, and shutdown. TrevRPC does not silently choose an embedded or Google Play services provider. Cronet owns its physical connection pool and reconnect behavior; TrevRPC never retries or replays an RPC.
Limits and Shutdown
HTTP/3 RPCs use the same frame, body, message, request, connection, deadline, idle-timeout, metrics, and graceful-shutdown controls as native QUIC and WebTransport.
An HTTP request reset cancels request-side work while leaving the connection reusable. Current Rust h3 dependencies do not expose a server-side future for response STOP_SENDING; response-only cancellation after request FIN is therefore observed on the next response write, deadline, connection close, or server shutdown.
Production endpoints require normal HTTPS certificate and hostname validation. Unlike browser WebTransport certificate hashes, ordinary HTTP/3 clients generally expect a publicly trusted, device-trusted, or explicitly configured private CA.