rpc_dart_http2 0.3.0
rpc_dart_http2: ^0.3.0 copied to clipboard
HTTP/2 caller/responder transports and server bootstrap for rpc_dart.
0.3.0 #
The largest release this transport has had. It is the one a gRPC deployment actually exposes, and most of what follows is what an unauthenticated peer could do to a server before it.
Breaking #
- The HTTP/2 wire machinery is no longer exported. Frame, stream and connection internals were public by omission; the transports, the server and the policy types are the supported surface.
pingIntervaldefaults to 30s on the server. A peer that completes the handshake and then goes silent used to hold its connection, endpoint and contracts forever. Set it to null for the old behaviour.- Requires rpc_dart 6. See its changelog — notably that a handler's bare
Exceptiontext no longer reaches the caller,closeOnProtocolErrornow defaults to false, and unary honoursRpcDataTransferMode.
Security #
- A peer that only sends violating frames is bounded.
_validateInboundported the half that had a name (closeOnProtocolError) and not the 256-violation backstop beside it: at the default policy, 2000 violating header blocks were all accepted, the connection stayed open, and RSS rose 27 MiB. The responder now closes on the flag or the backstop, the caller on the backstop alone — because killing a client's connection over one peer fault takes its other in-flight calls with it, and 256 times is no longer one bad frame. - Outbound metadata is checked against the security policy. Only inbound was
checked, so under
maxHeaders: 32, maxHeaderValueBytes: 64this side happily sent 64 headers, a 200-character value and a header name containing a space — all of which the shared layer refuses. - The peer's header block is bounded before it is converted, on the caller as well as the responder. This is the CONTINUATION flood: HTTP/2 exempts HEADERS from flow control, so the only bound is on size.
- A socket that never sends the HTTP/2 preface is dropped (
prefaceTimeout). A TCP SYN used to build a transport, an endpoint and the application's contracts before a byte arrived: 200 silent sockets gave 200 endpoints. - The CONNECT-proxy handshake is bounded in time and memory.
maxActiveStreamsis honoured on the caller, and the advertisedSETTINGS_MAX_CONCURRENT_STREAMSnow matches what the server enforces, clamped to what the field can carry.- gRPC is POST-only; the server used to execute a handler for any method.
- A request the policy rejects is answered, not dropped — and the refusal now
survives its own policy: under
maxHeaders: 1the trailer carrying bothgrpc-statusandgrpc-messagecould not be sent, and the peer was told "Response ended without a gRPC status" (UNAVAILABLE, which reads as retryable) for a deterministic rejection it must never retry. The status survives and the message gives way.
Fixed #
- Per-connection endpoints are closed when their connection ends. One
RpcResponderEndpointis created per socket and none were ever released. - Cancelled streams are aborted with RST_STREAM, the primitive HTTP/2 has for it, instead of a metadata frame that is illegal once this side has half-closed — and a peer's RST_STREAM is now delivered, so a cancelled call actually stops.
- Backpressure works in both directions. The caller no longer buffers its whole request for a stalled peer, an upload into a handler that is not consuming is throttled, and a slow reader genuinely slows the server down. A call that stalls is refused rather than pausing the connection's read loop, which would have stalled every other stream on it.
- A stream ended on DATA without trailers is not a clean end, and a truncated response stream is no longer reported as complete.
- Graceful shutdown: opt-in drain on
stop(), GOAWAY sent when draining, and receiving one no longer kills in-flight calls.close()no longer waits for streams it has already doomed. A draining connection is reported as draining rather than saturated, and one at MAX_CONCURRENT_STREAMS as saturated rather than dead. - Keepalive on both halves (caller and server PING), which is the only way to reclaim a half-open path.
- Reconnect: usable more than once, no longer orphans a connection per concurrent attempt, does not un-close a closed transport, and no longer restarts the stream-id sequence — a fresh id sequence after a reconnect reuses ids the peer still has state for.
- Status mapping: a peer RST_STREAM and a drained connection surface gRPC
statuses instead of
StateErrorand a raw transport exception; a non-200:statusmaps through the gRPC table; a 200 whose content-type is not gRPC is rejected; an over-limit request is answeredRESOURCE_EXHAUSTED. - A transport wrapper no longer drops the security policy, and a decorator that declares a capability can no longer switch the upload bound off.
- The endpoint is released when
onEndpointCreatedthrows. - The caller no longer kills its own connection after four calls.
- Nagle is off on every socket, as gRPC does.
0.2.4 #
-
BUG (non-ASCII regular header values were silently corrupted):
_headerValuebase64url-encoded any non-ASCII value on send but the decode side never reversed it, so the peer received a mangled string (the same asymmetry class as the-binbug). Per the gRPC HTTP/2 spec, ASCII metadata values must be printable ASCII (%x20-%x7E); a non-conforming value is now rejected with anArgumentErrorinstead of silently transformed. Binary or non-ASCII data must use a-binkey (base64). This also rejects CR/LF in values, closing a header injection vector. Tests intest/grpc_wire_compliance_test.dart. (grpc-messageis unaffected — the core layer percent-encodes it to ASCII before it reaches the transport.) -
BUG (
-binheader wire format was double-encoded and corrupted true binary): the metadata layer already stores-binvalues base64-encoded (e.g.base64Encode(statusDetailsBin)), but the HTTP/2 transport base64-encoded them AGAIN on send (_headerValue) and base64+utf8-decoded them on receive (http2HeadersToRpcMetadata). This double-processing was only self-consistent rpc_dart<->rpc_dart and broke interop with real gRPC peers in both directions; it also corrupted inbound binary that was valid UTF-8.-binvalues are now passed through verbatim on both send and receive (they are already the base64 string gRPC expects on the wire; the metadata getters decode on read). NOTE: this is a wire-format change forgrpc-status-details-binover HTTP/2 — a rpc_dart peer on <=0.2.3 will not interop with >=0.2.4 for status details. Regression tests intest/grpc_wire_compliance_test.dart(round-trips non-UTF8 binary). -
BUG (END_STREAM landed on the wrong message of a batch): when a single DATA frame parsed into multiple messages, both
RpcHttp2ResponderTransport(_handleIncomingData) andRpcHttp2CallerTransport(_handleDataMessage) detected the last message viamsgData == messages.last.messagesis aList<Uint8List>and==onUint8Listis identity-based, so the end-of-stream flag could land on an earlier element (e.g. when an earlier element shared the same object reference as the last). END_STREAM is now selected positionally — only the genuinely last element of the batch (i == messages.length - 1) is marked end-of-stream. Regression test:test/audit/end_of_stream_batch_test.dart.
0.2.3 #
- BUG (silent data loss on server-initiated streams):
RpcHttp2ResponderTransportsends (sendMetadata/sendMessage) now THROW aStateErrorwhen targeting a stream id that is not a known incoming (client-initiated) stream — i.e. an id minted bycreateStream()(server-push, unimplemented) or a stale/released id. Previously such sends logged a warning and returned, silently dropping the data. Legitimate unary/streaming responses, which reply on the client's stream id, are unaffected. Server-push remains unimplemented; the dead_outgoingStreamsmap (read but never populated) was removed along with its health/clear references.
0.2.2 #
Server-side hardening and a per-stream error-routing correctness fix.
- BUG A (security policy reachable):
RpcHttp2Servernow accepts aRpcSecurityPolicy(defaultconst RpcSecurityPolicy()) and forwards it to everyRpcHttp2ResponderTransport. Previously the server always used the default policy with no way to set one, somaxMessageLengthBytes/maxActiveStreamswere effectively unreachable. Also exposed onRpcHttp2Server.createWithContracts. - BUG B (per-stream error isolation): the caller and responder transports share
a single broadcast
StreamControllerforincomingMessages, andgetMessagesForStreamfiltered it bystreamId. Because.where()does not filter errors, an error on one stream was delivered to EVERY stream's subscriber (a parse error on stream 3 surfaced as an error on stream 5). Per-stream errors are now wrapped inRpcHttp2StreamErrorand routed only to the owning stream viafilterStreamEvents; connection-level fatal errors still fan out to all subscribers (correct). PublicincomingMessages/getMessagesForStreamAPI is unchanged. - BUG C (TLS / h2):
RpcHttp2Serveraccepts an optionalSecurityContext. When provided it binds aSecureServerSocketadvertising ALPNh2instead of a plaintextServerSocket; plaintext h2c remains the default. AddedRpcHttp2Server.isSecure. AddedRpcHttp2CallerTransport.viaSocket(...)so a TLSSecureSocket(with custom cert validation/pinning) can back the caller transport. Note: ALPN negotiation works at the wire level, butSecureSocket.selectedProtocolmay reportnullon some platforms (observed on the macOS Dart VM); the TLS h2 round-trip itself is verified by tests. - Fixed a latent "Concurrent modification during iteration" crash in
RpcHttp2Server.stop()(endpoint list mutated bysocket.doneduring close). RpcHttp2Server.portnow returns the OS-assigned port after binding when constructed with port0.
0.2.1 #
RpcHttp2CallerTransport: added optionalproxyUriparameter to bothsecureandinsecureconstructors — supports HTTP CONNECT proxy tunneling with optional Basic auth from URI userinfo.- Updated to
rpc_dart: ^3.1.0.
0.2.0 #
- Updated to
rpc_dart: ^3.0.0. - gRPC wire compliance fixes: correct trailers framing, binary headers, Trailers-Only responses.
RpcHttp2Server: supportsRpcReflectionRegistry.attachTo()for gRPC Server Reflection.
0.1.0 #
- Initial release: HTTP/2 caller/responder transports and
RpcHttp2Serverfor rpc_dart.