v0.10.0 — gRPC (HTTP/2 + HTTP/3), reactor-pool streaming, HTTP/3 streaming bodies
gRPC on HTTP/2 and HTTP/3, a streaming reverse path under the reactor pool, and HTTP/3 streaming request bodies — the async server now speaks full-duplex gRPC across both transports and streams worker responses across the reactor/worker split with credit-based backpressure. Plus RFC 9110 header combining, RFC 7692 window-bits negotiation, three ASAN-found use-after-frees fixed, and H3 hot-path work.
Added
- gRPC over HTTP/2 and HTTP/3 (#4) —
HttpServer::addGrpcHandler(); all four RPC shapes (unary, server/client-streaming, full-duplex bidi) with incrementalreadMessage()/writeMessage(), real HTTP trailers (grpc-status/grpc-message), grpc-web (binary + text), per-message gzip,grpc-timeout. Works under the reactor pool. - Reactor-pool streaming reverse path (#80) —
send()/writeMessage()/ SSE cross the thread boundary in FIFO order with native trailers at EOF; per-stream credit-based backpressure parks the producer over 1 MiB un-acked so a slow client cannot flood the shared reactor. - HTTP/3 streaming request bodies (#26) —
setBodyStreamingEnabled(true)applies the H2 Content-Length policy on H3, soreadBody()and incrementalreadMessage()work over QUIC with deferred flow-control credit. - WebSocket permessage-deflate honours
server_max_window_bits(RFC 7692).
Fixed
- HTTP/3 uploads larger than the initial stream window (256 KiB) no longer stall —
h3_recv_data_cbreturns QUIC credit as it consumes DATA. - Three latent use-after-frees found under ASAN (WebSocket dispose,
http_log_server_stopself-freeing write, H1 parser-borrowed request on reject/spawn-fail). - Duplicate request headers combined per RFC 9110 §5.3 across H1/H2/H3 (
Cookiewith"; ", others", "). - Inbound WebSocket message FIFO bounded (8×
max_message_size) → close 1013 on overflow. - Reactor-spawned listeners get their own per-listener counters slice (telemetry race).
Performance
- HTTP/3 reactor-pool hot paths: by-value mailbox commands, O(1) intrusive stream unlink, cached per-family sockaddr, thread-local CID-steering cipher, per-worker H3 static-delivery budget.
- H3 reactor-mode request bodies built persistent from the first byte; wire body adopted instead of re-copied.
- gRPC
readMessagereassembly compaction amortized.
Changed
- gRPC call-lifecycle policy extracted out of the transports into
src/grpc/grpc_call.c(#4). - Configure fails fast on a non-ZTS (NTS) PHP — the threaded worker pool requires ZTS by design.
gRPC in practice
A unary greeter — addGrpcHandler() deframes the request with readMessage() and frames the reply with writeMessage() (the 5-byte length prefixes are handled for you); a clean return defaults grpc-status: 0. gRPC shares the listener with plain HTTP, so a fallback HTTP handler is still registered:
<?php
use TrueAsync\HttpServer;
use TrueAsync\HttpServerConfig;
use TrueAsync\HttpRequest;
use TrueAsync\HttpResponse;
$server = new HttpServer(
(new HttpServerConfig())->addListener('127.0.0.1', 8080)
);
// Unary RPC: one request message in, one reply message out.
// Messages are your protobuf payloads.
$server->addGrpcHandler(function (HttpRequest $req, HttpResponse $resp) {
$name = $req->readMessage() ?? ''; // deframed request message
$resp->writeMessage("hello, {$name}"); // framed reply; grpc-status:0 on clean return
});
// gRPC shares the listener with plain HTTP; a fallback handler is still required.
$server->addHttpHandler(function (HttpRequest $req, HttpResponse $res) {
$res->setStatusCode(426)->setBody("This endpoint speaks gRPC.\n");
});
$server->start();Call it with any gRPC client, or raw over h2c (\x00 + 4-byte length + payload):
printf '\x00\x00\x00\x00\x05world' | curl --http2-prior-knowledge -s \
-H 'content-type: application/grpc' -H 'te: trailers' --data-binary @- \
http://127.0.0.1:8080/helloworld.Greeter/SayHello | tail -c +6; echo
# → hello, worldThe same handler covers the other three RPC shapes (server/client-streaming, full-duplex bidi) through incremental readMessage() / writeMessage(), and runs unchanged over HTTP/3.
Full changelog: v0.9.3...v0.10.0