rpc_dart 6.0.0 copy "rpc_dart: ^6.0.0" to clipboard
rpc_dart: ^6.0.0 copied to clipboard

Transport-agnostic RPC framework with contracts, streaming and included transports.

6.0.0 #

The theme is what a peer can make this side hold, and what leaves it without being asked for. Every item below was measured against a failing witness before it was fixed; the numbers are the reason each one is here.

Breaking #

  • Three RpcSecurityPolicy knobs are gone: maxWebSocketMessageBytes, maxChunkedMessageBytes, maxChunkCount. All three were documented "NOT CURRENTLY ENFORCED" and nothing outside security_policy.dart ever read one. With maxWebSocketMessageBytes: 1 MiB set, four 15 MiB messages were accepted and the connection stayed open. A control that is present, configurable and inert is worse than a missing one: an operator hardening a deployment stops looking. What actually bounds an inbound message is maxMessageLengthBytes, and that is now pinned by a test rather than claimed in a doc comment.
  • closeOnProtocolError defaults to false. Killing the connection over one bad frame takes every other in-flight call with it. A peer that only sends violating frames is still bounded, by a 256-violation backstop that closes the connection regardless of the flag.
  • Unary honours RpcDataTransferMode instead of guessing from the transport. On a transport reporting supportsZeroCopy: auto (the default) and zeroCopy take the object path as before; codec now serializes, which is what it always said. Writing codec used to change nothing. The mode is what a caller writes down to refuse the object path's costs — no size limit applies to an object, and a toJson that deliberately omits a field is not consulted at all when the object crosses a process boundary.
  • A response that ends without a grpc-status is reported as UNAVAILABLE on the channel transports too (websocket, isolate, wasm). http2 has said so since 4.x; the others ended the stream cleanly, so the same truncation was loud on one transport and silent on three, handing a consumer partial data as if it were complete. Client side only — a client's ordinary half-close carries no status and is not truncation. The end marker is withheld rather than followed by an error, because a cleanly closed stream swallows the error that arrives after it.
  • A handler error is default-deny on the wire. A bare Exception('...') thrown by a handler no longer has its text delivered; throw RpcStatusException, which also lets the handler choose a status instead of taking INTERNAL. Forwarding any Exception was defensible for code you wrote and not for the libraries under it — measured identically on http2, websocket and isolate, callers received "boom, path = '/etc/private/key.pem'", "refused, address = 127.0.0.1, port = 5432" and "_SecretException: db-password-hunter2". An allow-list of the leaky types cannot work, because most belong to packages this library has never heard of. RpcStatusException and rpc_dart's own RpcException hierarchy still travel intact.
  • A transport that cannot carry the stream-id watermark is refused at attach. A decorator that dropped IRpcStreamIdSequence silently restarted the id sequence after a reconnect, so a new call reused an id the peer still had state for. Transports now continue their sequence across a reconnect (RpcChannelTransport.resumeStreamIdsAfter / lastIssuedStreamId).
  • Thirteen pipeline internals are no longer exported from package:rpc_dart/rpc_dart.dart: CallProcessor, StreamProcessor, the two pipeline mixins, RpcResponderStreamState, RpcResponderStreamStore, RpcResponderMethodRegistry, RpcResponderMethodBinding, RpcResponderPingHandler, RpcEndpointPingProtocol, RpcEndpointPingExchange, RpcEndpointPingResult, RpcLongTimer. 152 public types became 139; nothing in this workspace used any of them.
  • dart:typed_data is no longer re-exported. Import it directly. Measured across the workspace: zero files needed a new import.
  • Registering a duplicate method name throws from one place, not eight — the four registration shapes had four copies of the check with different wording.

Security #

Each of these is reachable by an unauthenticated peer on an open connection.

  • A metadata flood is bounded. Metadata is exempt from flow control by design (HTTP/2 exempts HEADERS because a control frame that cannot be sent deadlocks the stream it is trying to end), and the per-stream controller that receives it was unweighed and uncapped: 4000 frames, 32 MiB, none paced and no bound fired. Frames queued for a stream are now charged against maxBufferedBytes; over the bound the STREAM fails with RESOURCE_EXHAUSTED and the connection survives, because a peer flooding one call must not take down the others sharing the socket. A consumer that keeps up is unaffected.
  • A queued header is charged for what it retains, not for its text. ["h1","v1"] weighed 4 bytes and retained ~100. At 500 headers per frame — the attacker's optimum, where the weighed total stays under the bound right up to the event ceiling — 4096 frames were admitted for 190 MiB retained.
  • The pre-method budget counts metadata too. It charged payload?.length ?? 0, so 4000 metadata frames were charged 0.00 MiB and pinned 789 MiB against a 16 MiB ceiling.
  • Two peer-keyed collections were unbounded: _statusSeen and the finished-stream set, both keyed by ids the peer chooses.
  • The frame channel and the reconnect proxy buffer their inbound streams, so a frame arriving before anything listens is no longer dropped or held unbounded.
  • maxConcurrentHandlers (new, default null) bounds running handlers, not just stream state. A handler that ignores its cancellation token cannot be preempted, so reclaiming its stream returns the admission slot while the work continues. Against maxActiveStreams: 4, one call every 250 ms with a 40 ms deadline: 37 concurrent handlers after 20 s and growing, versus 4 with the new limit. It is invisible while it happens — activeStreams read 0 at the moment 37 handlers were running — and saturating the connection hides it, so a load test reports the ceiling holding. Pacing is what defeats it.

Fixed #

  • A unary call whose request stream fails is answered, instead of the failure being logged while the caller waits out its deadline.
  • Flow control: connection credit is repaid for bytes nobody consumed even when a consumer is attached (a paused consumer never receives done, so the repay never ran and the bytes stayed owed forever — 1024 → 3072 KiB); a zero grant is participation, not a legacy peer that should be flooded; a grant for a stream that has ended is refused instead of resurrecting its credit.
  • A throwing state callback no longer ends the isolate.
  • The stream-id cursor survives a transport's own close().
  • Connection-pool credit is exposed for diagnostics (health()).

Performance #

  • Log message strings are no longer built for levels that discard them. 158 interpolating call sites across core and the transports were counted and guarded; the 26 constant-message calls were left alone, because a const string costs nothing to build.
  • The guards ask the filter's own question. isInternal/isTrace/isDebug omitted the tag, which _resolveLevel consults ahead of everything else — so under a tag override the guard predicted the filter wrongly in the mute direction, silencing 222 call sites that should have logged.
  • One context token per call instead of two, drawn in 3 syscalls instead of 12.

Changed #

  • The analysis floor was raised (strict language modes plus a wider lint set) and the 320 issues under it fixed. Internal, but it is why this release touches nearly every file.
  • RpcStreamRouter and the drain loop are extracted rather than copied into each transport — four and three copies respectively, now one each, tested once.

5.0.1 #

Fixed #

  • Detached teardown in the responder pipeline no longer raises into the zone. Cancellations, stream teardown and error replies run when no caller is left to report to, and a Dart server with no zone handler exits on an uncaught error — a remote peer could end its host. Failures while binding a handler are still reported to the waiting peer as INTERNAL rather than swallowed.

Changed #

  • registerServiceContract documents the ownership rule. A server endpoint's lifetime is one connection and closing it disposes the contracts registered on it, so a contract owns what it built for that connection and borrows everything shared with the rest of the process.

5.0.0 #

A hardening release. The theme is resources a peer can consume without authenticating, and calls that ended without telling anyone.

Breaking #

  • Flow control is on by default. Streams now carry a per-stream window (flowControlWindowBytes, 4MB) and a connection-wide one (flowControlConnectionWindowBytes, 64MB). Credit rides on bare metadata frames, which a peer that predates this ignores, so a mixed-version pair degrades to the old behaviour rather than deadlocking — but every connection now puts one advertisement frame on the wire, which matters if you assert on exact frame counts. Set either to null to disable; transports with their own flow control (HTTP/2) should.
  • An expired deadline is reported as RpcDeadlineExceededException on every call shape. Unary previously surfaced a bare TimeoutException, and a deadline landing exactly on the boundary could end a stream silently.
  • A stream that ends without a trailer now raises UNAVAILABLE. It used to close cleanly, so a consumer whose connection died mid-stream could not tell a truncated result from a complete one.
  • An in-flight unary call whose stream ends now fails immediately instead of hanging until its deadline (or the 60s fallback).
  • Registering the same method name twice on a contract throws. It used to be a silent overwrite where the last registration won — including across shapes, so a unary call could be answered by a server-stream handler. Registration no longer re-runs setup() when the caller has already called it.
  • Inbound metadata is checked against the security policy. A peer exceeding maxHeaders, maxHeaderNameBytes, maxHeaderValueBytes, maxMethodPathLength or maxMetadataBytes is rejected, and with closeOnProtocolError (default) the transport closes. Defaults are generous; conforming traffic is unaffected.
  • RpcList decoding throws FormatException on non-object elements or a non-list items field, instead of returning a partial or empty list.
  • The CBOR writer refuses ints outside ±2^53, the range its own reader supports on every platform, instead of emitting bytes it cannot decode. Carry larger values as a String or two 32-bit halves.

Security #

Each of these is reachable by an unauthenticated peer on an open connection.

  • maxActiveStreams is enforced on streams the PEER opens. It was only ever checked for locally-initiated calls, so the one limit that mattered for a server bounded nothing: against a limit of 3, a peer opened 500 concurrent streams and all 500 were accepted.
  • Half-open streams are reclaimed (halfOpenStreamTimeout, 60s). A stream sits half-open from its metadata frame until a handler is dispatched, and only the peer's optional grpc-timeout bounded that, so metadata-only frames parked responder state indefinitely — about 33 KiB each, per connection.
  • Flow control bounds what an unread stream can pin. With a paused consumer, a server-stream handler ran 19,000 messages (311 MB) ahead; a 1MB window holds it to 0.7 MB. Both directions of every shape are covered.
  • Flow-control grants are clamped to the window this side configured. The grant value is peer-controlled and was taken at face value, so two frames granting 1 TB lifted a paused stream from 0.8 MB in flight to 300.6 MB.
  • Flow-control bookkeeping is capped at maxActiveStreams. The maps are keyed by peer-chosen stream ids, so 50,000 frames naming ids that never became streams left 50,000 permanent entries — and, for data frames, 50,000 window-update frames sent back.
  • Frame overhead counts against maxMessageLengthBytes, and the context header caps apply to the merged result rather than only the incoming map.
  • The cancellation control headers are reserved, so user metadata can no longer cancel its own call at the door.
  • The rate limiter's bounds and the config guards survive release builds; they were asserts, which Dart strips.
  • The CBOR decoder's indefinite-length string readers are bounded.

Fixed #

  • Connection loss now stops work. In-flight handlers were left running with nowhere to send, their cancellation tokens never fired, and their stream state was never reclaimed — one abandoned handler and one leaked stream per dropped connection, on both sides.
  • Client-streaming handlers receive messages as they arrive. Nothing started one until the peer half-closed, so the whole request was buffered first: an upload that never half-closed pinned 494 MB while the handler had consumed nothing. This is what the shape is for.
  • Consumer demand reaches the producer. pause() on a returned stream stopped delivery to the listener and nothing else; every stage above the transport kept decoding messages nobody had asked for.
  • Cancellation and deadlines are honoured on every shape: a stalled client stream, an idle bidirectional stream (which could not be cancelled at all), a bidirectional stream whose server finishes first, a client stream whose deadline was silently capped at 60s, and a handler that outlives its deadline. Cancelled streams now abort via the transport's reset primitive (RST_STREAM) where one exists.
  • A bidirectional call that sends no request, and a client stream that carries no messages, both work. The first was rejected outright; the second never reached the server, which then waited out the caller's timeout.
  • Structured error details survive on every shape, including bidirectional and zero-copy unary, where they were silently dropped while status and message came through.
  • Leaks: the reconnect proxy orphaning transports, a caller endpoint never draining the transport's global stream (900 messages retained after 300 calls), streaming calls tracked from hand-out rather than first listen, the rate limiter's background timer, finished-stream bookkeeping, addStream's source subscription, and the ping stream id on pre-send failures.
  • A call-scope disposer can no longer block shutdown. One that never completes used to hold close() open forever; disposers are now bounded (RpcCallScope.disposerTimeout) and cleanup failures are neither swallowed nor allowed to strand the rest.
  • Decoding: protobuf field tags are read as varints, unknown wire types inside RetryInfo are skipped, and RpcList.filled is growable like every other constructor.
  • The retry backoff no longer sleeps past the call deadline; a rate-limited stream stops at the first rejection; concurrent drains await the same completion; a contract unregisters by service name rather than key prefix; the circuit breaker releases its half-open gate on an inconclusive probe; unary parser state is per call.

Performance #

  • Per-stream routing restored in the reconnect proxy, replacing a broadcast filter evaluated once per active stream per message.

4.3.3 #

Fixes #

  • Server streams are metered at establishment, not per response. Charging a token for every emitted response let a server-paced burst trip the per-key rate limit and inject RESOURCE_EXHAUSTED into a healthy long-lived subscription (notify feeds, tailing, large chunked downloads), tearing it down until the client re-subscribed. A server stream now costs one token at open (like a unary call). Opt back into per-response accounting with meterServerStreamMessages: true. Inbound metering (unary, client-stream, and bidi requests) is unchanged.

4.3.2 #

Fixes #

  • Server-stream completion no longer poisons a reused context. The caller pipeline fired the context cancellation token unconditionally from the bridge's onCancel, which also runs on normal completion — so a successfully-finished server stream cancelled its own RpcContext. When one context is shared across calls (e.g. a blob download that fetches a manifest then its chunks and calls throwIfCancelled between), the next call threw RpcCancelledException even though the previous one succeeded. Propagation is now guarded on !finished; real mid-stream cancellation is unaffected.

4.3.1 #

Fixes #

  • Client stream IDs no longer leak. A CallProcessor/UnaryCaller call that ends without a clean finishSending (cancellation, deadline, error) — or whose constructor throws after allocating the id (e.g. an already-expired deadline) — now releases its transport stream id. Previously a long-lived client eventually failed with "Too many active streams".
  • Server-stream cancellation propagates to the server. Cancelling the consumer's subscription to a serverStream now fires the call's cancellation token (sending the grpc-status=CANCELLED trailer), so the server stops producing for an abandoned stream instead of streaming to nobody.
  • CBOR decode is robust to malformed input: a truncated map key throws FormatException instead of an uncaught RangeError, and an 8-byte unsigned integer outside the 53-bit safe range is rejected rather than silently wrapping (negative on the VM, lossy on dart2js).
  • gRPC frame length is read as an unsigned 32-bit value; a length with the high bit set no longer wraps negative on dart2js.
  • RpcNum operator ~/ is platform-stable. It no longer threw on the VM while returning a value on dart2js for a whole-valued operand.
  • RateLimit.slidingWindow / RateLimit.tokenBucket assert max > 0, window > 0, and burst > 0 instead of dividing by zero / producing NaN at runtime.
  • Error-detail (google.rpc.Status) decoders skip unknown protobuf wire types instead of aborting, so an unrecognized field before details no longer drops the entire details list.
  • Logger: redaction recurses into lists (sensitive fields nested in a list element were leaking); redactString matches on word boundaries and tolerates spaces around =/:; and a throwing log output is isolated so it neither breaks the caller nor starves the other outputs.

4.3.0 #

Features #

  • RpcContext now carries an injectable clock (defaults to DateTime.now), so deadline logic (isExpired, remainingTime, withTimeout) is testable without real time. Inject one with context.withClock(...).
  • Responder handlers can use a per-call RpcCallScope for automatic resource cleanup. It was documented but never wired (always returned null); the server now injects one scope per incoming call and closes it when the call ends — by success, error, cancellation, or deadline — running registered disposers in LIFO order. Reach it via context.callScope (nullable) or context.requireCallScope(); register cleanup with scope.onDispose(...), auto-cancel subscriptions with scope.listen(...), or acquire-and-clean-up in one expression with scope.use(resource, dispose).

Fixes #

  • Circuit breaker: a success from a call that began while the breaker was closed no longer re-closes a breaker that other concurrent calls have since opened. _onSuccess is state-aware — only a half-open probe closes the circuit.
  • Rate limiter: the per-key dynamic counter maps are bounded by maxTrackedKeys (default 100000) with LRU eviction. The key comes from a caller-supplied keyExtractor, so an unbounded map was a memory-exhaustion vector — a client rotating keys can no longer grow it without limit.
  • Frame reassembly is O(n) instead of O(n^2): a peer dribbling one frame across many tiny chunks forced a full buffer recopy per chunk (a cheap CPU amplification). The receive buffer now grows geometrically (amortized O(1) append).
  • CBOR: a toJson() that throws now propagates the error instead of silently encoding the object's toString() onto the wire as if it were the payload.

Behavior changes (technically breaking; minimal real-world impact) #

  • CBOR encoding now throws ArgumentError on a non-string map key instead of silently coercing it via toString(). Such keys never round-tripped (decoding always yields string keys) and could collapse distinct keys (1 and '1').
  • BufferedBroadcastController overflow is now fatal: on exceeding maxPendingEvents it delivers the surviving prefix, surfaces a StateError with the dropped count, and closes the stream, rather than silently dropping events and continuing with a gap. Overflow only happens when no consumer ever drained the buffer (an effectively dead connection).

Internal #

  • De-duplicated the contract codec-mode resolution (responder/caller/peer) and the request/response frame-send path.
  • All comments, log messages, and exception text are now English; emoji removed from doc comments.

4.2.3 #

  • Transports no longer drop inbound frames received before the RPC pipeline subscribes. A transport starts consuming the connection as soon as it is up, but the pipeline listens to incomingMessages slightly later; a plain broadcast controller drops events delivered with no listener, so the first frames on a cold connection (e.g. a client-stream's leading chunk) were lost, surfacing as "First chunk must carry blobId and vaultId".
  • New BufferedBroadcastController<T> (implements StreamSink<T>): a broadcast controller that queues events while unlistened and flushes them in arrival order on the first listen, then forwards live. It is the buffering core of HTTP/2's StreamMessageQueueIn. Leak-safe: the queue is cleared on close and bounded by maxPendingEvents.
  • Applied uniformly to all transports: RpcChannelTransport (WebSocket / isolate / WASM), the HTTP/2 caller + responder, the HTTP caller + responder, and the WebSocket caller's reconnect-stable forward controller.

4.2.2 #

  • Responder no longer drops a stream's payload frames that arrive before its metadata (headers) frame. On a broadcast transport (no replay) the first data frame can be observed before the headers right after a connection opens; the pipeline used to discard any payload whose method was not yet known, losing the leading frame (e.g. a client-stream upload whose first chunk carries routing metadata). Such frames — and an early end-of-stream — are now buffered and replayed in arrival order once the metadata resolves the method.

4.2.1 #

  • RpcRateLimiter rejections now carry gRPC status RESOURCE_EXHAUSTED (8) instead of being mapped to INTERNAL (13) on the wire. RpcRateLimitException is now an RpcStatusException, so clients recognise rate-limit rejections as retryable and RpcRetryInterceptor backs off and retries instead of failing.

4.2.0 #

Deadline handling, a wire-format bug fix, and header hardening — adopted from a review of the gRPC client/server implementation.

Fixes:

  • encodeGrpcTimeout picked the largest unit "that fits in 8 digits" and accepted a zero value, so EVERY sub-hour timeout encoded to 0H, zeroing the deadline on the wire. It now uses the finest unit that fits (e.g. 5s5000000u), preserving the value exactly. This was invisible until the server began enforcing deadlines.
  • Outgoing client requests strip protocol-reserved headers (content-type, te, user-agent, grpc-timeout) from user-supplied metadata, so user headers can no longer clobber protocol framing/negotiation. Added RpcHeaders.te / RpcHeaders.userAgent / RpcHeaders.isReserved. grpc-encoding / grpc-accept-encoding are intentionally NOT reserved — rpc_dart routes compression negotiation through the call context.

Features:

  • The server now enforces the client's grpc-timeout deadline: it arms a timer and, on expiry, cancels the handler's cancellation token (the same path drain() uses). Dart cannot preempt a bare await, so a purely non-cooperative handler's future still completes in the background, but cooperative handlers (checking the token / isExpired) and request-stream readers unwind, and the response path is torn down.
  • The default client call timeout (used when no deadline is set) is raised from 30s to 60s. Note: this is a client-local .timeout() and is not propagated as grpc-timeout; set a context deadline to have the server enforce it too.

4.1.1 #

Transport hot-path performance.

Performance:

  • RpcChannelTransport now routes incoming messages to a dedicated per-stream controller instead of every getMessagesForStream caller adding a .where(streamId == id) listener to one shared broadcast. The old approach delivered each message to all active-stream listeners and re-filtered it per stream (~O(streams^2) work), which collapsed aggregate throughput under many concurrent streams. With routing, throughput no longer degrades under load (measured +24% at 500 concurrent streams in-process). Per-call latency is unchanged.

Behavioral note:

  • getMessagesForStream now returns a single-subscription stream (one listener per stream id, buffered until listened) rather than a broadcast-derived one. This matches the one-call-one-stream contract; transports and endpoints use it this way already. Code that listened to the same stream id more than once would need to fan out itself.

4.1.0 #

Error-semantics fixes, two concurrency fixes, and hot-path performance.

Behavioral change:

  • The raw responses getter on the server-stream and bidirectional callers now surfaces a non-OK grpc-status trailer as an error (it used to complete silently — only .call() / .payloadResponses threw). Code that iterated responses directly will now observe the error instead of a silent finish.

Fixes:

  • Zero-copy unary/bidi error paths forward the real RpcStatusException status code + message (+ details where supported) instead of hardcoding INTERNAL.
  • Server-stream call() no longer swallows RpcCancelledException.
  • A compressed payload without grpc-encoding now throws RpcStatusException (not a bare RpcException), so status-code branching matches.
  • RpcContextBuilder.inheritFrom keeps a non-null parent's cancellation token, deadline and headers even when its traceId is null.
  • send() queues the transmit synchronously, so send() immediately followed by sendError() / finishSending() no longer drops the last message or sends the trailer first.
  • Rate limiter re-resolves the counter per element, so cleanup-eviction of an in-flight stream's counter cannot double the effective limit.
  • Circuit breaker subscribes to the half-open probe source eagerly (+ safety timeout), so an abandoned/never-listened probe stream cannot wedge it half-open.

Performance (behavior-preserving):

  • RpcMessageFrame.encode: single Uint8List + setRange (was a boxed list, byte-by-byte copy, then a second copy).
  • CBOR map-key sort encodes each key's UTF-8 bytes once (was ~3x per key).
  • Metadata caches the parsed service/method and the accept-encoding header.
  • channel_frame.decodeAll reads each frame header once.
  • Hot-path internal log calls are guarded by isInternal, so the message string is not built when logging is off (the default).

4.0.0 #

First release since 3.3.0. Rolls up the unreleased 3.4.0 - 3.5.1 work below.

BREAKING:

  • RpcTransportRouter and StreamDistributor were removed from this package and moved to rpc_notify. Import them from there if you used them.
  • Metadata header values must be printable ASCII (%x20-%x7E) on every transport; non-ASCII / binary must use a -bin (base64) key, otherwise validateMetadata throws ArgumentError. (This is why it is a major bump.)

See the 3.4.0 - 3.5.1 sections below for the full list of fixes (zero-copy retry, redaction, transport-closed detection, cancellation delivery, varint codec, router subscription leak, etc.).

3.5.1 #

  • Moved RpcTransportRouter and StreamDistributor out to rpc_notify.

Behavioral change:

  • Metadata header values must now be printable ASCII (%x20-%x7E) on ALL transports. RpcSecurityPolicy.isValidHeaderValue previously rejected only CR/LF/NUL and let arbitrary non-ASCII through; the value was then silently corrupted by some transports (e.g. HTTP/2 base64url-encoded it on send but never decoded it). Per the gRPC HTTP/2 spec, ASCII-valued metadata must be printable ASCII; binary or non-ASCII data must use a -bin key (base64), and human-readable text belongs in the message body or the percent-encoded grpc-message. Non-conforming values now throw ArgumentError at validateMetadata time on every send path (this hook is already called by RpcChannelTransport — and thus isolate/WebSocket/WASM — and by the HTTP/1.1 and HTTP/2 transports). This also uniformly blocks CR/LF header injection. New regression test (test/audit/audit_header_ascii_test.dart).

Bug fixes:

  • LogRedactor now recurses into nested maps of any generic type. The recursion guard entry.value is Map<String, Object> only matched that exact type and skipped Map<String, dynamic> / Map<String, String> / untyped Map -- i.e. virtually every JSON-decoded map -- so sensitive fields nested inside such maps leaked into logs unredacted. Redaction now recurses into ANY Map, stringifies keys, compares them case-insensitively as before, and tolerates null values. New regression test (test/audit/audit_redaction_nested_map_test.dart) proves a sensitive key nested in a Map<String, dynamic> and in a deeply nested untyped map is redacted.
  • Transport-closed detection in StreamProcessor no longer swallows unrelated errors. The response/trailer/error send paths in base_processor.dart decided a send failure was "transport closed" via e.toString().contains('closed'), which silently dropped ANY error whose text merely contained "closed" (e.g. "database connection was closed"), losing real send failures. Detection is now by exception type and exact message -- StateError('Transport is closed'), the signal network transports (HTTP/1.1, HTTP/2) actually throw -- via a shared _isTransportClosed helper; the broad substring clause is removed and the genuine closed case is logged at debug. Behavior for the real transport-closed case is unchanged. New regression test (test/audit/audit_transport_closed_match_test.dart).
  • RpcTransportRouter no longer leaks a live response subscription on stream-id reuse. _subscribeToResponsesForStream did _responseSubscriptions[clientStreamId] = subscription unconditionally; because client stream IDs are reused over the router's lifetime, a re-route of the same ID before END_STREAM cleanup overwrote a still-live subscription without cancelling it -- leaking it and letting the stale subscription keep forwarding responses from the old server stream. The existing subscription for the key is now cancelled before reassignment. New regression test (test/audit/audit_router_subscription_reuse_test.dart).
  • Cancellation is no longer dropped when it fires before the consumer subscribes. The four cancellation-delivery blocks in base_processor.dart (request/response controllers in both StreamProcessor and CallProcessor) gated addError on && <controller>.hasListener. The controllers are single-subscription StreamController<T>() (not broadcast), where addError before a listener attaches is buffered and delivered on subscription -- so the hasListener gate skipped buffering and lost the RpcCancelledException forever whenever cancellation raced ahead of the consumer's listen. The gate is removed (only !isClosed remains), and the empty catch (_) {} that hid real delivery failures is replaced with a debug log including the stream id. New regression test (test/audit/audit_cancellation_before_listen_test.dart) cancels before subscribing and asserts the late subscriber still receives the RpcCancelledException on both processors.
  • error_details.dart varint codec now handles negative and large values correctly. _writeVarint used while (v > 0x7F) { ...; v >>= 7; }, which never emitted the protobuf-mandated 10-byte unsigned two's-complement encoding for negative signed ints, and v >>= 7 is undefined past 32 bits on dart2js. _readVarint used result |= (byte & 0x7F) << shift, which on dart2js silently overflows once shift >= 32, corrupting any varint above 2^32 or any negative (10-byte) value. Both reachable signed fields were affected: the google.rpc.Status code (int32, can be negative) and the RetryInfo Duration seconds (int64 -- a negative Duration yields negative seconds, and a long delay can exceed 2^32 seconds). The writer now encodes negatives per spec (full unsigned 64-bit two's-complement = 10 bytes) and the reader reconstructs signed/large values, both using 32-bit hi/lo halves so no shift exceeds 31 bits (the dart2js-safe technique already used for uint64 in special_cbor.dart). New regression test (test/audit/audit_error_details_varint_test.dart) covers negative codes, negative and > 2^32 RetryInfo seconds, and large length-delimited payloads, green on VM and dart2js (-p chrome -c dart2js).

3.5.0 #

Behavioral changes:

  • Zero-copy unary path now throws typed RpcStatusException instead of a plain Exception. RpcCallerPipeline._executeUnaryCall (the zero-copy unary code path) previously threw an untyped Exception('gRPC error ...') for a non-OK trailer status and Exception('gRPC error 14: No response received') when the stream closed with no response. Because these were plain Exceptions, the gRPC status was only stringified, so callers, the circuit breaker failureOn, and the retry predicate could not read it. This directly undermined the 3.4.0 conservative retry default: that default retries ONLY RpcStatusException with UNAVAILABLE/RESOURCE_EXHAUSTED, and the "No response received" (dropped connection) case is exactly the UNAVAILABLE case it is meant to retry -- but on the zero-copy path it threw a plain Exception, so the retry never fired despite the 3.4.0 CHANGELOG claiming it does. Both sites now throw RpcStatusException: the non-OK trailer carries the actual status code and decoded grpc-message, and the no-response case is RpcStatusException(RpcStatus.unavailable, 'No response received'). The conservative retry default now actually fires on the zero-copy "no response" path. Same fix applied to the client stream caller (rpc/streams/client/caller.dart): "Stream closed without response payload" -> RpcStatusException(INTERNAL), "Stream closed without receiving response" -> RpcStatusException(UNAVAILABLE). New regression test asserts the thrown type and status on the zero-copy path and that the default retry retries it.

Robustness:

  • Removed sync: true from two stream controllers to eliminate a reentrancy hazard. RpcChannelTransport._incomingCtl (broadcast) and RpcStreamBaseProcessor._responseController delivered events synchronously on the caller's stack, so a listener re-entering the transport (e.g. calling createStream/send from within onData) could race or mutate state mid-dispatch. Both now deliver asynchronously (events scheduled on the microtask queue), removing the reentrancy race. Full suite stays green on VM and -p node -- sync delivery was not load-bearing for ordering or back-pressure.

3.4.0 #

Behavioral changes:

  • RpcRetryInterceptor default retry predicate is now conservative and gRPC-aligned. BEHAVIORAL: when no explicit retryOn is provided, retries happen ONLY on clearly-transient errors -- RpcStatusException with status UNAVAILABLE (14) or RESOURCE_EXHAUSTED (8) (the connection/transport-closed and overload signals the framework surfaces on the wire, e.g. a dropped connection becomes UNAVAILABLE "No response received"), plus the local RpcRateLimitException. Previously the default retried EVERY error except cancellation/deadline, which re-issued non-idempotent unary calls (e.g. a write that committed server-side but lost its response), causing duplicate side effects. Generic RpcException, INTERNAL/INVALID_* status codes, and non-RPC exceptions are no longer retried by default. An explicit retryOn still fully overrides the default.
  • RpcContextUtils.generateTraceId() now generates a real unique random id (Random.secure() with a seeded fallback), format trace_<base64url-16-bytes>. BEHAVIORAL (format): previously the id was a deterministic pure function of the millisecond timestamp (timestamp * 31 + 17), so two trace ids generated in the same millisecond were IDENTICAL -- corrupting distributed-trace correlation under concurrency. The trace_ prefix is preserved; the old trace_<timestamp>_<n> shape is gone.

Security / robustness:

  • CBOR decoder now bounds container nesting depth (_maxDepth = 256). A crafted deeply-nested array/map/tag payload previously recursed without limit and could trigger a StackOverflowError (DoS from untrusted bytes); the decoder now throws FormatException('CBOR nesting too deep') instead. Applies to arrays, maps and tag chains on both the decode and decodeUnsafe paths. All existing CBOR + parity tests stay green on VM and -p node.
  • Receive-path frame hardening (untrusted peer). RpcChannelFrame.decode/decodeAll and RpcFrameMultiplexedChannel now bound the RECEIVE/decode path against a hostile peer, sourcing limits from the RpcSecurityPolicy already carried by RpcChannelTransport (threaded into the frame channel via RpcChannelTransport.fromChannel/pair). Previously the policy was consulted only on the SEND side and the decode path trusted the declared frame length. Three fixes:
    • Remote OOM: a frame header declaring an oversized payload (payloadLen up to 4 GiB) is now rejected from the header alone -- the oversized payload is never sliced or buffered. decodeAll/decode take an optional maxPayloadLen and throw the new typed RpcFrameException when the declared length exceeds RpcSecurityPolicy.maxMessageLengthBytes. The frame channel also caps the reassembly buffer at RpcSecurityPolicy.effectiveMaxBufferedBytes, so a peer dribbling bytes toward a huge declared frame is stopped before unbounded buffering. On violation the channel surfaces RpcFrameException on the incoming stream and closes.
    • Receive-loop crash on malformed metadata: _decodeMetadataPayload previously did unchecked casts on attacker JSON (as Map<String,dynamic>, map['p'] as String?, h[0]/h[1] as String), so a top-level array, malformed JSON, invalid UTF-8, or non-string header values threw TypeError/FormatException synchronously out of the listen callback as an unhandled async error. Decoding is now fully defensive: UTF-8, JSON object shape, methodPath type, and each header entry (a list of exactly two strings) are validated, yielding a typed handled RpcFrameException surfaced via the incoming stream instead of an uncaught throw into the zone.
    • Decompression bomb: the built-in dart:io gzip fallback now decodes in a chunked manner and aborts as soon as the accumulated output would exceed the policy limit, instead of fully materializing the expansion before the post-decompress size check. RpcCompressionCodec.decompress, RpcGrpcCompression.decompress, and the parser's decompressor hook gained an optional maxOutputBytes; RpcMessageParser passes its maxMessageLength so a tiny gzip bomb is rejected with a FormatException before full expansion. The existing post-decompress backstop check remains for codecs that ignore the hint. New regression tests cover all three on VM, with the frame/metadata cases also green on -p node.

Fixes:

  • RpcCircuitBreakerInterceptor reset-timeout timing now uses a monotonic Stopwatch instead of DateTime.now() subtraction. A backward wall-clock jump (NTP correction, manual clock change) previously made the elapsed-since-last-failure go negative or huge, so the breaker could either never half-open or half-open instantly. Behavior under a normal clock is unchanged (covered by the existing resetTimeout half-open tests). The class does not expose a clock injection seam, so the monotonic source is internal.

3.3.1 #

  • RpcClientConnection.forceReconnect() now resets the attempt counter and resumes even after disconnect() -- previously it could leave the connection stuck in Offline (the reconnect loop exited immediately because _isStopped was still set) or inherit a stale attempt count and hit maxAttempts early. Documented that in-flight calls do not survive a reconnect (only the endpoint and new calls do) and that maxAttempts counts from the initial drop. Added regression tests.
  • Removed the rpc_dart_compression dev-dependency. The compression negotiation test now registers a small test-local reversible codec under the gzip encoding instead of pulling in RpcGzipCodec, so the published package has no path dependency. No runtime/API change.

3.3.0 #

Features / changes:

  • RpcRateLimiter now does PER-MESSAGE accounting on streaming RPCs. Potentially behavioral: previously a streaming call consumed a single token at stream open regardless of message count; the default is now that EVERY message on the message-bearing direction (server-stream responses, client-stream/bidi requests) counts against the limit. On mid-stream exhaustion the wrapped stream emits an RpcRateLimitException error (gRPC RESOURCE_EXHAUSTED) instead of silently dropping. Unary behavior is unchanged.
  • RpcCircuitBreakerInterceptor half-open is now single-probe: exactly ONE probe is admitted in halfOpen; concurrent calls are rejected with CircuitBreakerOpenException until the probe resolves (success -> close, failure -> reopen). Previously every call was allowed through in halfOpen, flooding a recovering service.
  • CBOR decoder now handles CBOR tags (skips tag, reads tagged value) and the undefined simple value (maps to null). Previously these threw FormatException on the production CborCodec.decode path.
  • CBOR robustness: the decoder now also decodes indefinite-length arrays, maps, text strings and byte strings; IEEE 754 half (16-bit) and single (32-bit) floats; and extended one-byte simple values.
  • CBOR readers/writers unified: the redundant slow reference reader (_CborReader) and the static _encode* writer family were removed. CborCodec.decodeUnsafe now decodes via the single fast reader (_FastCborReader.readValue, any top-level value to dynamic) and CborCodec.encodeUnsafe now encodes via the single fast writer (_FastCborWriter.writeValue, arbitrary top-level values with lax map keys). Return types and byte output are unchanged: nested maps remain Map<String, dynamic>, arrays List<dynamic>, byte strings Uint8List; encode/encodeUnsafe stay byte-compatible (guarded by the parity test). This removes the prior risk of the two readers/writers silently diverging.
  • CBOR encoder hardening on dart2js: a non-finite double (Infinity/-Infinity/NaN) classified as an int under JavaScript's single number type is now encoded as an IEEE 754 double instead of crashing in the 64-bit integer writer (Result of truncating division is Infinity). _writeUint64BigEndian also throws a clear FormatException if a non-finite value ever reaches it. Additionally, a finite integer-valued double of magnitude >= 2^64 (e.g. 1e300) also reports is int under JavaScript and previously overflowed the integer encoder, silently decoding back to 0; such values are now encoded as IEEE 754 doubles and round-trip correctly. Native-VM ints (bounded by int64) are unaffected.
  • Added test/serializers/cbor_parity_test.dart, a corpus-based decode/round-trip guard (int boundaries incl. >2^32/>2^53/negatives, doubles incl. NaN/Infinity/-0.0, unicode/empty strings, empty/non-empty byte arrays, tags, indefinite lengths, deep nesting). Runs on VM and -p node. After the reader/writer unification it asserts that encode(x) round-trips through decode, that decode and decodeUnsafe agree, that encode and encodeUnsafe are byte-identical, and that hand-crafted exotic CBOR decodes to expected values.
  • The "Unsupported grpc-encoding" error (caller, responder pipeline, and the compression registry) now hints at the fix: on web/dart2js the built-in dart:io gzip is unavailable, so register a cross-platform codec (e.g. RpcGzipCodec.register() from package:rpc_dart_compression). Message text only; behavior unchanged.
  • test/streams/compression_test.dart now registers RpcGzipCodec (added rpc_dart_compression as a dev-dependency) so the gzip server-stream/client-stream/bidi roundtrip tests exercise compression cross-platform and pass identically on VM and -p node/-p chrome, instead of relying on the VM-only built-in gzip.
  • Fixed a dart2js (-p node/-p chrome) hang when cancelling a long-lived server-stream subscription on the caller side. RpcCallerEndpoint.serverStream previously returned () async* { yield* stream }() over a chain of suspended async*/await for generators (handleServerStream -> response middleware -> ServerStreamCaller.call). On dart2js, await sub.cancel() against that chain — while the server stream was still open (e.g. gRPC Health Watch) — never resolved, so the cancel deadlocked and any caller awaiting it timed out. The caller-facing stream is now bridged through an explicit StreamController whose onCancel releases request tracking and fires the inner cancel without awaiting it, making cancellation deterministic and identical on VM and dart2js. Server-stream emission, ordering, and resource cleanup are unchanged; VM and -p node now both pass the grpc_health Watch tests.

3.2.3 #

Fixes (audit):

  • CBOR 8-byte integer READ was truncated on dart2js ((result << 8) uses 32-bit JS bitops) -- now reads hi/lo 32-bit halves and recombines, mirroring the write-side fix. Values above 2^32 now round-trip correctly in the browser.
  • Circuit breaker was a no-op for streaming RPCs: it recorded success as soon as the Stream object was returned. Errors emitted during stream production now count as failures; an open circuit on a stream RPC surfaces as a stream error instead of a synchronous throw.
  • TransportRouter leaked the subscription, stream maps and server stream id when sendMetadata threw -- failed sends now roll back all state.
  • error_details decoding now validates length-delimited bounds and bounds the varint loop, throwing FormatException on malformed input instead of an uncaught RangeError.
  • ResponderPipeline no longer creates per-stream state for junk control frames (unbounded-memory vector); drain() now actually closes the remaining streams on timeout instead of only logging.
  • RpcRateLimiter no longer repopulates its counters after dispose() (added _disposed guard) and accepts an injectable monotonic clock via nowMicros (default monotonic, no longer wall-clock sensitive).

Behavioral changes:

  • RpcNum/RpcInt/RpcDouble.operator == no longer throws for a non-RpcNum operand -- it returns false per the Dart == contract.
  • RpcNum.fromJson/RpcInt.fromJson/RpcDouble.fromJson now throw FormatException on malformed input instead of silently returning 0.

3.2.2 #

Fixes:

  • Fixed CBOR encoder using ByteData.setUint64 which is unsupported in dart2js -- replaced with JS-safe 32-bit writes
  • Removed applied RPC log types (RpcLogOutput, RpcLogResponder, RpcLogServiceResponder, RpcLogServiceCaller) -- moved to rpc_dart_log package

Logger stabilization:

  • Added LogOutput.writeAsync() and isAsync for async output backends (database, HTTP)
  • Added LogController.addEnricher() / removeEnricher() / setRedactor() for runtime pipeline management
  • Added LogController(clock:) for deterministic timestamps in tests -- propagated through LogScope and LogSpanHandle

3.2.1 #

Fixes:

  • Fixed LogController.add() redundant enricher condition that was always true
  • Fixed LogRedactor not redacting error messages and log message text -- now matches field=value / field:value patterns in strings
  • Fixed ConsoleOutput JSON key escaping -- keys containing ", \, newlines are now properly escaped
  • Fixed RingBufferOutput using fake LogEvent placeholder -- replaced with nullable list
  • Fixed RpcLogOutput.flush() silently swallowing send errors -- added droppedCount counter
  • Removed dead noSuchMethod override from noop span handle
  • Added RpcResponderEndpoint.setLogController() for post-construction logger injection

3.2.0 #

Built-in logging system:

  • New LogController / LogScope architecture replaces old RpcLogger
  • Pipeline: level filter -> sampling -> enrichers -> redaction -> outputs
  • LogScope.noop for zero-cost disable when no logger configured
  • sealed LogRecord with three types: LogSpanStart, LogEvent, LogSpan
  • Endpoints accept LogController? logger parameter

Spans (operations with duration):

  • withSpan() / startSpan() API for measuring operation timing
  • Spans bypass level filter (telemetry, not logs) — available in production even with minLevel = fatal
  • spansEnabled flag for opt-out
  • Span ID (6 hex chars) links events to their span in console output

Console output (logfmt-style):

  • One line per record, structured key=value fields
  • Three formats: pretty (colored), json, compact
  • traceId shown automatically when present
  • Span start (>>), events, and span end with duration on separate lines

Responder auto-logging:

  • context.log in responder handlers auto-configured with scope, traceId, requestId
  • Scope includes service and method name: rpc.{endpoint}.{Service}.{method}
  • Works for all stream types (unary, server, client, bidirectional)

Production features:

  • SamplingConfig — per-level rate limiting
  • LogEnricher — auto-attach fields (host, pid, etc.) to every record
  • LogRedactor — strip sensitive fields (password, token) from data
  • RingBufferOutput — in-memory history, queryable by LogFilter

RPC log transport:

  • RpcLogOutput — send logs to remote peer with offline buffer
  • RpcLogResponder — accept remote logs into local controller
  • RpcLogServiceResponder — expose subscribe stream + history + remote control
  • RpcLogServiceCaller — client API for remote diagnostics

Breaking:

  • Removed RpcLogger, RpcLoggerColors, AnsiColor, RpcLoggerLevel, RpcContextAwareLogger, RpcContextualLogging
  • Removed extension methods: logRpcError, logRpcWarning, logStreamBound, logStreamFinished, logMessageReceived
  • Endpoint constructors: loggerColors parameter removed, logger parameter now takes LogController?

3.1.1 #

Bug fix:

  • Fixed Uint8List CBOR encoding on dart2js: added TypedData fallback in _FastCborWriter and _encodeValue to ensure typed byte arrays are always encoded as CBOR byte strings (major type 2), not integer arrays (major type 4). Without this fix, blob uploads from dart2js clients produced ~1.9x inflated byte counts on the server, causing "declared length does not match received bytes" errors.

3.1.0 #

Structured error model:

  • RpcStatusException now carries typed details list — structured error information sent via grpc-status-details-bin trailer (wire-compatible with standard gRPC google.rpc.Status).
  • Added RpcErrorDetail hierarchy: RpcBadRequest (field violations), RpcRetryInfo (retry delay), RpcDebugInfo (stack traces), RpcErrorInfo (reason/domain/metadata), RpcRawErrorDetail (unknown types passthrough).
  • RpcStatusException.fromTrailer() factory reconstructs typed exceptions from wire data on the caller side.
  • All four caller patterns (unary, server-stream, client-stream, bidirectional) now throw RpcStatusException instead of generic Exception on gRPC errors.
  • All responder patterns forward statusDetailsBin in error trailers when handler throws RpcStatusException with details.
  • Minimal protobuf encoding/decoding for google.rpc.Status and google.protobuf.Any — no external protobuf dependency.

Graceful drain:

  • Added RpcResponderEndpoint.drain({Duration timeout}) — initiates graceful shutdown: rejects new streams with UNAVAILABLE, cancels active stream contexts via cancellation token, waits for completion.
  • Responder pipeline now attaches RpcCancellationToken to all incoming stream contexts automatically — handlers can listen for cancellation during shutdown.
  • RpcApp._drainEndpoints() (framework) now calls endpoint.drain() in parallel instead of polling.

Other:

  • Added RpcPeerContract — base class for bidirectional contracts where either side can initiate calls. Extends RpcResponderContract and exposes callUnary, callServerStream, callClientStream, callBidirectionalStream methods bound to the same RpcPeerEndpoint.
  • RpcPeerEndpoint is now exported from the library public API.
  • RpcServiceKind enum added to annotations.dart (unidirectional / peer) for use with @RpcService(kind: ...).
  • @RpcService gained grpcDescriptor flag (default false) — when true, the generator emits a grpcDescriptor static field for gRPC Server Reflection registration.
  • RpcContextUtils.generateTraceId() is now public (was _generateTraceId).
  • RpcResponderContract.serviceName supports dynamic suffix via serviceNameSuffix setter — used for scoped/isolated service registrations.
  • Internal refactor: caller_pipeline.dart and responder_pipeline.dart extracted as separate part files; logic unchanged.

3.0.1 #

  • RpcMessageParser: replaced List<int> backing buffer with Uint8List — eliminates integer boxing and reduces message extraction from two copies to one.
  • special_cbor.dart: fixed JS-unsafe 64-bit integer encoding — replaced >> 56 / >> 48 / ... bit-shift chains with ByteData.setUint64, which is correctly implemented on both Dart VM and dart2js.
  • RpcContext.withAdditionalHeaders: removed duplicate _sanitizeHeaders call that could silently drop headers when the merged map exceeded the 128-header or 64 KB limit.
  • RpcCallerEndpoint: changed compressionEnabled default from true to false to avoid compatibility issues with servers that do not advertise compression support.
  • Removed CORD-specific API (createChain, forBusinessOperation, forDomainCall, extractDomainMetadata, DomainMetadata, RpcContextAware, correlationId).

3.0.0 #

Breaking changes and major additions:

3-layer transport architecture (IRpcChannel / IRpcMultiplexedChannel / RpcChannelTransport):

  • IRpcChannel — minimal raw byte pipe interface
  • IRpcMultiplexedChannel — multiplexed message channel between channel and transport
  • RpcFrameMultiplexedChannel — wraps IRpcChannel with 9-byte frame codec
  • RpcDirectMultiplexedChannel — zero-copy in-memory paired channel
  • RpcChannelTransportIRpcTransport wrapper with .pair(), .memoryPair(), .fromChannel() factories
  • RpcInMemoryTransport.pair() is now a thin delegate to RpcChannelTransport.memoryPair()

Resilience primitives (moved from rpc_dart_framework to core):

  • RpcRetryInterceptor — configurable retry with BackoffPolicy (constant, linear, exponential)
  • RpcCircuitBreakerInterceptor — circuit breaker with open/half-open/closed states
  • RpcClientConnection — reconnect state machine with health monitoring
  • RpcRateLimiter — pluggable rate limiting with per-key dynamic limits

gRPC Health Checking Protocol:

  • RpcGrpcHealthService — implements grpc.health.v1.Health (Check + Watch)
  • RpcGrpcHealthClient — typed client for health checks

Other:

  • RpcBinaryCodec<T> now works with any T extends Object (not just IRpcSerializable)
  • RpcStatusException(statusCode, message) — handlers can return specific gRPC status codes
  • dart:typed_data re-exported via rpc_dart.dart (Uint8List available without extra import)
  • Removed onServiceRegistered callback from RpcResponderEndpoint

2.6.3 #

  • Added RpcRemoved annotation. Marks an inherited RPC method as removed in a versioned contract. The generator produces a @Deprecated + throw UnsupportedError implementation so callers receive a compile-time warning and a clear runtime error message.

2.6.2 #

  • RpcMessageParser: replaced per-message buffer slicing with a read-offset approach — advance() moves a pointer in O(1) and a single compact() at the end of each parse pass drops consumed bytes in O(remaining), eliminating the previous O(N²) copy behaviour when multiple gRPC frames arrive in a single chunk (relevant for HTTP/2 and WebSocket transports).
  • RpcMessageParser: fixed a latent bug where the loop condition buffer.length >= 5 prevented re-entry after a 5-byte header arrived without a body shorter than 5 bytes, causing such messages to stall in the buffer indefinitely.

2.6.1 #

  • IRpcServer: removed host and port from the interface — these are transport-level concerns, not RPC server concerns.
  • IRpcServerFactory interface removed along with RpcHttp2ServerFactory and RpcWebSocketServerFactory — the abstraction had no consumers.

2.6.0 #

  • Added RpcHeaders class with gRPC semantic header name constants; all internal metadata and protocol code now uses RpcHeaders.* instead of raw strings.
  • Core parser passes compressed frames through when no decompressor is configured so the application layer (transport) can handle decompression — core stays agnostic.
  • Transport packages now own their own wire-format: pseudo-headers (:method, :path, :status, etc.) are generated by each transport and never stored in RpcMetadata; http2HeadersToRpcMetadata filters them out automatically.
  • RpcHttpCorsPolicy: always exposes grpc-encoding, grpc-status, grpc-message, grpc-accept-encoding in Access-Control-Expose-Headers so browsers do not block them in cross-origin responses.
  • RpcHttpCorsPolicy: always allows grpc-timeout, grpc-encoding, grpc-accept-encoding in Access-Control-Allow-Headers.
  • RpcHttpCorsPolicy: exposedHeaders parameter renamed to extraExposedHeaders to clarify it is additive on top of the required gRPC headers.
  • RpcCallerEndpoint: injects grpc-accept-encoding into context based on compressionEnabled so globally-registered codecs do not force server-side compression when the endpoint has compression disabled.
  • WebSocket transport: metadata is now encoded as binary CBOR (WsMetadataCodec) instead of JSON — ~40 % more compact, self-describing, no extra dependencies; wire format: {"h":[["name","val"],...],"p":"/Svc/Method"}.
  • WebSocket transport: _ensureGrpcFrame normalises parser output to gRPC frames, consistent with HTTP/2 transport delivery to incomingMessages.
  • Exported CborCodec from rpc_dart for use by transport packages.

2.5.1 #

  • Compression is now pluggable via RpcCompressionCodec interface and RpcGrpcCompression.register().
  • Native dart:io gzip continues to auto-register on startup as before.
  • External packages (e.g. rpc_dart_compression) can now register codecs on web/JS/Wasm.

2.5.0 #

  • Added gzip compression support: RpcCallerEndpoint now has a compressionEnabled flag (default true) that automatically injects grpc-encoding: gzip for non-zero-copy transports (e.g. network transports).
  • StreamProcessor auto-detects request/response encoding from incoming metadata (grpc-encoding / grpc-accept-encoding), enabling transparent decompression on the server side.
  • UnaryResponder captures grpc-accept-encoding per stream to compress unary responses when the client advertises support.
  • RpcMetadata.forServerInitialResponse now accepts an optional encoding parameter to include grpc-encoding in the initial server response headers for streaming compression.
  • Added compression tests for all stream types (unary, server streaming, client streaming, bidirectional).
  • Fix: UnaryResponder now captures grpc-encoding from incoming client metadata per stream and uses it for decompression, matching the behaviour of StreamProcessor.
  • Fix: caller and streaming processors now validate grpc-encoding against supported encodings before compressing and throw RpcException with a clear message instead of UnsupportedError.
  • Fix: UnaryResponder now throws RpcException (was bare Exception) when the request frame is empty.
  • Refactor: per-stream state in UnaryResponder consolidated from five parallel maps into a single _UnaryStreamState object — cleanup is now atomic.
  • Refactor: response-encoding negotiation logic extracted to RpcGrpcCompression.selectResponseEncoding() and reused by both UnaryResponder and StreamProcessor.
  • Refactor: RpcCallerEndpoint._effectiveContext renamed to _prepareContext for clarity.

2.4.1 #

  • Add interface for transport server

2.4.0 #

  • Breaking: removed transport toolkit (transport_toolkit.dart) from the public API surface.
  • Added RpcSecurityPolicy to centralize transport/parser limits and metadata validation.
  • Security: grpc-message is now percent-encoded per gRPC HTTP/2 spec; added decodeGrpcMessage helper.
  • Protocol: client request metadata now includes grpc-accept-encoding (advertises supported message encodings).
  • Fix: clean up RpcCallerEndpoint cancellation token registry after completed calls (prevents memory growth in long-lived clients).
  • Security: RpcInMemoryTransport now enforces policy limits (metadata validation, active stream cap); pair() accepts an optional policy.

2.3.4 #

  • fix lints package compatibility

2.3.3 #

  • fix test package compatibility

2.3.2 #

  • Added annotations for codegeneration

2.3.1 #

  • Optimize ping requests
  • Introduced a unified middleware and interceptor pipeline across caller and responder endpoints, ensuring context propagation for unary and streaming RPC flows.
  • Added RpcMiddlewareContext enhancements and default async hooks so extensions can enrich request/response handling without touching the core.
  • Hardened serialization by validating codec decoders and shipping a pluggable RpcBinaryCodec for external binary formats such as protobuf.
  • Expanded the test suite with exhaustive pipeline coverage for all RPC shapes and binary codec scenarios.

2.3.0 #

  • Added aggregated diagnostics for caller and responder endpoints with health()/reconnect() snapshots that combine endpoint metrics and transport status via RpcEndpointHealth and RpcHealthStatus.
  • Implemented a built-in ping protocol between endpoints that exposes round-trip timing, responder metadata and debug labels through RpcCallerEndpoint.ping().
  • Extended transport infrastructure with IRpcTransport.health() and IRpcTransport.reconnect() plus detailed implementations for RpcInMemoryTransport, RpcTransportRouter and the transport toolkit, including automatic partner shutdown to avoid close deadlocks.
  • Improved RpcStreamIdManager so stream identifiers are recycled after hitting the HTTP/2 limit, preventing allocation failures during long-lived workloads.
  • Updated documentation and examples with health monitoring guidance and diagnostics-driven workflows.

2.2.2 #

  • Docs: simplify everything

2.2.1 #

  • Fix: remove base class modifier to allow mock contracts

2.2.0 #

  • Added support for cancellation in caller/responder
  • Fix timeout passing through headers

2.1.1 #

  • Update logo

2.1.0 #

  • Added dispose base method to responder to cleanup resources

2.0.0 #

  • Updated license to MIT
  • Added logo to the package
  • Added readme translations (RU, EN)

1.8.0 #

  • Added ability to specify data transfer mode in contract (zero-copy, codec, auto)

1.7.0 #

  • Added ability to specify whether transport supports Zero-Copy

1.6.0 #

  • Added Zero-Copy optimization for RpcInMemoryTransport - object transfer without serialization/deserialization

1.5.0 #

  • Added Transport Router for smart routing of RPC calls between transports
  • Added typedef RpcRoutingCondition for typing routing condition functions
  • Added support for routing rules with priorities (routeCall, routeWhen)
  • Added conditional routing capability with access to RpcContext
  • Added automatic validation of transport roles (client/server)
  • Implemented correct Stream ID routing between transports
  • Added router statistics and detailed logging
  • Updated documentation with Transport Router usage examples
  • RpcLoggerSettings -> RpcLogger
  • RpcContextPropagation -> RpcContext

1.4.0 #

  • Added RpcContext API with full gRPC-style context support
  • Added support for headers, metadata, deadline and timeout
  • Added distributed tracing support with trace ID
  • Added internal logging level for library internal details
  • Eliminated log duplication, library is "silent" by default
  • Optimized InMemoryTransport for improved performance
  • Fixed race conditions and deadlock situations
  • Increased reliability of tests and CI/CD pipeline

1.3.2 #

  • Removed auto-start of Responders
  • Removed bundleId from StreamDistributor
  • Updated documentation

1.3.1 #

  • Optimized CBOR serializer and deserializer
  • Added benchmarks for performance testing

1.3.0 #

  • Updated documentation

1.2.2 #

  • Added 1ms delay for data transfer stability

1.2.1 #

  • Fixed specific errors in rpc-method operations (timeouts)

1.2.0 #

  • Fixed critical bug with stream processing in Stream Processor
  • Added explicit support for bindToMessageStream() method for manual stream binding
  • Improved error handling in streams through gRPC statuses in metadata
  • Fixed deadlock situations in client, server and bidirectional streams
  • Optimized timeouts in tests for faster execution
  • Improved documentation on working with streams and error handling
  • Fixed issue with double stream listening in ClientStreamResponder

1.1.0 #

  • Added RpcStreamIdManager for stream ID management

1.0.3 #

  • CBOR serializer now works only with Map<String, dynamic>

1.0.2 #

  • Added StreamDistributor
  • Fixed linter issues

1.0.1 #

  • Added subcontract registration
  • Fixed unary method operations

1.0.0 #

  • First stable release
  • Implemented contract-based Backend-for-Domain (BFD) architecture
  • Added support for all RPC types: unary calls, server streaming, client streaming, bidirectional streaming
  • Added efficient CBOR serialization
  • Added primitive types (String, Int, Double, Bool, Null) with operator support
  • Implemented extensible logging system with color and level support
  • Added universal transports: InMemoryTransport and IsolateTransport
  • Implemented timeout handling and informative errors
  • Main package contains only platform-independent transports, platform-specific ones will be available in separate packages

0.2.0 #

  • Improved stream handling (BidiStream, ClientStreamingBidiStream, ServerStreamingBidiStream)
  • Added support for diagnostic metrics and monitoring
  • Improved marker handling in streams for more reliable interaction
  • Added typed markers for various operations (stream completion, timeouts, etc.)
  • Improved error handling and status transfer between client and server
  • Optimized metadata handling in requests and responses
  • Improved deadline and timeout handling in RPC operations
  • Added operation cancellation mechanism

0.1.1 #

  • Fixed error when registering contracts
  • Added MsgPack serializer

0.1.0 #

  • Initial release
5
likes
160
points
1.74k
downloads

Documentation

API reference

Publisher

verified publisherrpc.nogipx.dev

Weekly Downloads

Transport-agnostic RPC framework with contracts, streaming and included transports.

Homepage
Repository (GitHub)
View/report issues

Topics

#rpc

License

MIT (license)

More

Packages that depend on rpc_dart