rpc_dart 4.3.3
rpc_dart: ^4.3.3 copied to clipboard
Transport-agnostic RPC framework with contracts, streaming and included transports.
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_EXHAUSTEDinto 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 withmeterServerStreamMessages: 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 ownRpcContext. When one context is shared across calls (e.g. a blob download that fetches a manifest then its chunks and callsthrowIfCancelledbetween), the next call threwRpcCancelledExceptioneven 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/UnaryCallercall that ends without a cleanfinishSending(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
serverStreamnow fires the call's cancellation token (sending thegrpc-status=CANCELLEDtrailer), 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
FormatExceptioninstead of an uncaughtRangeError, 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.tokenBucketassertmax > 0,window > 0, andburst > 0instead 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 beforedetailsno longer drops the entire details list. - Logger: redaction recurses into lists (sensitive fields nested in a list
element were leaking);
redactStringmatches 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 #
RpcContextnow carries an injectableclock(defaults toDateTime.now), so deadline logic (isExpired,remainingTime,withTimeout) is testable without real time. Inject one withcontext.withClock(...).- Responder handlers can use a per-call
RpcCallScopefor 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 viacontext.callScope(nullable) orcontext.requireCallScope(); register cleanup withscope.onDispose(...), auto-cancel subscriptions withscope.listen(...), or acquire-and-clean-up in one expression withscope.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.
_onSuccessis 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-suppliedkeyExtractor, 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'stoString()onto the wire as if it were the payload.
Behavior changes (technically breaking; minimal real-world impact) #
- CBOR encoding now throws
ArgumentErroron a non-string map key instead of silently coercing it viatoString(). Such keys never round-tripped (decoding always yields string keys) and could collapse distinct keys (1and'1'). BufferedBroadcastControlleroverflow is now fatal: on exceedingmaxPendingEventsit delivers the surviving prefix, surfaces aStateErrorwith 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
incomingMessagesslightly 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>(implementsStreamSink<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'sStreamMessageQueueIn. Leak-safe: the queue is cleared on close and bounded bymaxPendingEvents. - 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 #
RpcRateLimiterrejections now carry gRPC status RESOURCE_EXHAUSTED (8) instead of being mapped to INTERNAL (13) on the wire.RpcRateLimitExceptionis now anRpcStatusException, so clients recognise rate-limit rejections as retryable andRpcRetryInterceptorbacks 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:
encodeGrpcTimeoutpicked the largest unit "that fits in 8 digits" and accepted a zero value, so EVERY sub-hour timeout encoded to0H, zeroing the deadline on the wire. It now uses the finest unit that fits (e.g.5s→5000000u), 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. AddedRpcHeaders.te/RpcHeaders.userAgent/RpcHeaders.isReserved.grpc-encoding/grpc-accept-encodingare intentionally NOT reserved — rpc_dart routes compression negotiation through the call context.
Features:
- The server now enforces the client's
grpc-timeoutdeadline: it arms a timer and, on expiry, cancels the handler's cancellation token (the same pathdrain()uses). Dart cannot preempt a bareawait, 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 asgrpc-timeout; set a context deadline to have the server enforce it too.
4.1.1 #
Transport hot-path performance.
Performance:
RpcChannelTransportnow routes incoming messages to a dedicated per-stream controller instead of everygetMessagesForStreamcaller 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:
getMessagesForStreamnow 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
responsesgetter on the server-stream and bidirectional callers now surfaces a non-OKgrpc-statustrailer as an error (it used to complete silently — only.call()/.payloadResponsesthrew). Code that iteratedresponsesdirectly will now observe the error instead of a silent finish.
Fixes:
- Zero-copy unary/bidi error paths forward the real
RpcStatusExceptionstatus code + message (+ details where supported) instead of hardcodingINTERNAL. - Server-stream
call()no longer swallowsRpcCancelledException. - A compressed payload without
grpc-encodingnow throwsRpcStatusException(not a bareRpcException), so status-code branching matches. RpcContextBuilder.inheritFromkeeps a non-null parent's cancellation token, deadline and headers even when itstraceIdis null.send()queues the transmit synchronously, sosend()immediately followed bysendError()/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: singleUint8List+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.decodeAllreads 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:
RpcTransportRouterandStreamDistributorwere removed from this package and moved torpc_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, otherwisevalidateMetadatathrowsArgumentError. (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.isValidHeaderValuepreviously 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-binkey (base64), and human-readable text belongs in the message body or the percent-encodedgrpc-message. Non-conforming values now throwArgumentErroratvalidateMetadatatime on every send path (this hook is already called byRpcChannelTransport— 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:
LogRedactornow recurses into nested maps of any generic type. The recursion guardentry.value is Map<String, Object>only matched that exact type and skippedMap<String, dynamic>/Map<String, String>/ untypedMap-- i.e. virtually every JSON-decoded map -- so sensitive fields nested inside such maps leaked into logs unredacted. Redaction now recurses into ANYMap, stringifies keys, compares them case-insensitively as before, and toleratesnullvalues. New regression test (test/audit/audit_redaction_nested_map_test.dart) proves a sensitive key nested in aMap<String, dynamic>and in a deeply nested untyped map is redacted.- Transport-closed detection in
StreamProcessorno longer swallows unrelated errors. The response/trailer/error send paths inbase_processor.dartdecided a send failure was "transport closed" viae.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_isTransportClosedhelper; 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). RpcTransportRouterno longer leaks a live response subscription on stream-id reuse._subscribeToResponsesForStreamdid_responseSubscriptions[clientStreamId] = subscriptionunconditionally; 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 bothStreamProcessorandCallProcessor) gatedaddErroron&& <controller>.hasListener. The controllers are single-subscriptionStreamController<T>()(not broadcast), whereaddErrorbefore a listener attaches is buffered and delivered on subscription -- so thehasListenergate skipped buffering and lost theRpcCancelledExceptionforever whenever cancellation raced ahead of the consumer'slisten. The gate is removed (only!isClosedremains), and the emptycatch (_) {}that hid real delivery failures is replaced with adebuglog 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 theRpcCancelledExceptionon both processors. error_details.dartvarint codec now handles negative and large values correctly._writeVarintusedwhile (v > 0x7F) { ...; v >>= 7; }, which never emitted the protobuf-mandated 10-byte unsigned two's-complement encoding for negative signed ints, andv >>= 7is undefined past 32 bits on dart2js._readVarintusedresult |= (byte & 0x7F) << shift, which on dart2js silently overflows onceshift >= 32, corrupting any varint above 2^32 or any negative (10-byte) value. Both reachable signed fields were affected: thegoogle.rpc.Statuscode(int32, can be negative) and theRetryInfoDurationseconds(int64 -- a negativeDurationyields 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 inspecial_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
RpcStatusExceptioninstead of a plainException.RpcCallerPipeline._executeUnaryCall(the zero-copy unary code path) previously threw an untypedException('gRPC error ...')for a non-OK trailer status andException('gRPC error 14: No response received')when the stream closed with no response. Because these were plainExceptions, the gRPC status was only stringified, so callers, the circuit breakerfailureOn, and the retry predicate could not read it. This directly undermined the 3.4.0 conservative retry default: that default retries ONLYRpcStatusExceptionwithUNAVAILABLE/RESOURCE_EXHAUSTED, and the "No response received" (dropped connection) case is exactly theUNAVAILABLEcase it is meant to retry -- but on the zero-copy path it threw a plainException, so the retry never fired despite the 3.4.0 CHANGELOG claiming it does. Both sites now throwRpcStatusException: the non-OK trailer carries the actual status code and decodedgrpc-message, and the no-response case isRpcStatusException(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: truefrom two stream controllers to eliminate a reentrancy hazard.RpcChannelTransport._incomingCtl(broadcast) andRpcStreamBaseProcessor._responseControllerdelivered events synchronously on the caller's stack, so a listener re-entering the transport (e.g. callingcreateStream/sendfrom withinonData) 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:
RpcRetryInterceptordefault retry predicate is now conservative and gRPC-aligned. BEHAVIORAL: when no explicitretryOnis provided, retries happen ONLY on clearly-transient errors --RpcStatusExceptionwith statusUNAVAILABLE (14)orRESOURCE_EXHAUSTED (8)(the connection/transport-closed and overload signals the framework surfaces on the wire, e.g. a dropped connection becomesUNAVAILABLE"No response received"), plus the localRpcRateLimitException. 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. GenericRpcException,INTERNAL/INVALID_*status codes, and non-RPC exceptions are no longer retried by default. An explicitretryOnstill fully overrides the default.RpcContextUtils.generateTraceId()now generates a real unique random id (Random.secure()with a seeded fallback), formattrace_<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. Thetrace_prefix is preserved; the oldtrace_<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 aStackOverflowError(DoS from untrusted bytes); the decoder now throwsFormatException('CBOR nesting too deep')instead. Applies to arrays, maps and tag chains on both thedecodeanddecodeUnsafepaths. All existing CBOR + parity tests stay green on VM and-p node. - Receive-path frame hardening (untrusted peer).
RpcChannelFrame.decode/decodeAllandRpcFrameMultiplexedChannelnow bound the RECEIVE/decode path against a hostile peer, sourcing limits from theRpcSecurityPolicyalready carried byRpcChannelTransport(threaded into the frame channel viaRpcChannelTransport.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 (
payloadLenup to 4 GiB) is now rejected from the header alone -- the oversized payload is never sliced or buffered.decodeAll/decodetake an optionalmaxPayloadLenand throw the new typedRpcFrameExceptionwhen the declared length exceedsRpcSecurityPolicy.maxMessageLengthBytes. The frame channel also caps the reassembly buffer atRpcSecurityPolicy.effectiveMaxBufferedBytes, so a peer dribbling bytes toward a huge declared frame is stopped before unbounded buffering. On violation the channel surfacesRpcFrameExceptionon the incoming stream and closes. - Receive-loop crash on malformed metadata:
_decodeMetadataPayloadpreviously 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 threwTypeError/FormatExceptionsynchronously 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 handledRpcFrameExceptionsurfaced via the incoming stream instead of an uncaught throw into the zone. - Decompression bomb: the built-in
dart:iogzip 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'sdecompressorhook gained an optionalmaxOutputBytes;RpcMessageParserpasses itsmaxMessageLengthso a tiny gzip bomb is rejected with aFormatExceptionbefore 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.
- Remote OOM: a frame header declaring an oversized payload (
Fixes:
RpcCircuitBreakerInterceptorreset-timeout timing now uses a monotonicStopwatchinstead ofDateTime.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 afterdisconnect()-- previously it could leave the connection stuck inOffline(the reconnect loop exited immediately because_isStoppedwas still set) or inherit a stale attempt count and hitmaxAttemptsearly. Documented that in-flight calls do not survive a reconnect (only the endpoint and new calls do) and thatmaxAttemptscounts from the initial drop. Added regression tests.- Removed the
rpc_dart_compressiondev-dependency. The compression negotiation test now registers a small test-local reversible codec under thegzipencoding instead of pulling inRpcGzipCodec, so the published package has no path dependency. No runtime/API change.
3.3.0 #
Features / changes:
RpcRateLimiternow 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 anRpcRateLimitExceptionerror (gRPC RESOURCE_EXHAUSTED) instead of silently dropping. Unary behavior is unchanged.RpcCircuitBreakerInterceptorhalf-open is now single-probe: exactly ONE probe is admitted inhalfOpen; concurrent calls are rejected withCircuitBreakerOpenExceptionuntil the probe resolves (success -> close, failure -> reopen). Previously every call was allowed through inhalfOpen, flooding a recovering service.- CBOR decoder now handles CBOR tags (skips tag, reads tagged value) and the
undefinedsimple value (maps tonull). Previously these threwFormatExceptionon the productionCborCodec.decodepath. - 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.decodeUnsafenow decodes via the single fast reader (_FastCborReader.readValue, any top-level value to dynamic) andCborCodec.encodeUnsafenow 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 remainMap<String, dynamic>, arraysList<dynamic>, byte stringsUint8List;encode/encodeUnsafestay 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 anintunder 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)._writeUint64BigEndianalso throws a clearFormatExceptionif a non-finite value ever reaches it. Additionally, a finite integer-valued double of magnitude>= 2^64(e.g.1e300) also reportsis intunder JavaScript and previously overflowed the integer encoder, silently decoding back to0; such values are now encoded as IEEE 754 doubles and round-trip correctly. Native-VMints (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 thatencode(x)round-trips throughdecode, thatdecodeanddecodeUnsafeagree, thatencodeandencodeUnsafeare 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()frompackage:rpc_dart_compression). Message text only; behavior unchanged. test/streams/compression_test.dartnow registersRpcGzipCodec(addedrpc_dart_compressionas 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.serverStreampreviously returned() async* { yield* stream }()over a chain of suspendedasync*/await forgenerators (handleServerStream-> response middleware ->ServerStreamCaller.call). On dart2js,await sub.cancel()against that chain — while the server stream was still open (e.g. gRPC HealthWatch) — never resolved, so the cancel deadlocked and any caller awaiting it timed out. The caller-facing stream is now bridged through an explicitStreamControllerwhoseonCancelreleases 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 nodenow both pass thegrpc_healthWatchtests.
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
Streamobject 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. TransportRouterleaked the subscription, stream maps and server stream id whensendMetadatathrew -- failed sends now roll back all state.error_detailsdecoding now validates length-delimited bounds and bounds the varint loop, throwingFormatExceptionon malformed input instead of an uncaughtRangeError.ResponderPipelineno 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.RpcRateLimiterno longer repopulates its counters afterdispose()(added_disposedguard) and accepts an injectable monotonic clock vianowMicros(default monotonic, no longer wall-clock sensitive).
Behavioral changes:
RpcNum/RpcInt/RpcDouble.operator ==no longer throws for a non-RpcNumoperand -- it returnsfalseper the Dart==contract.RpcNum.fromJson/RpcInt.fromJson/RpcDouble.fromJsonnow throwFormatExceptionon malformed input instead of silently returning0.
3.2.2 #
Fixes:
- Fixed CBOR encoder using
ByteData.setUint64which is unsupported in dart2js -- replaced with JS-safe 32-bit writes - Removed applied RPC log types (
RpcLogOutput,RpcLogResponder,RpcLogServiceResponder,RpcLogServiceCaller) -- moved torpc_dart_logpackage
Logger stabilization:
- Added
LogOutput.writeAsync()andisAsyncfor async output backends (database, HTTP) - Added
LogController.addEnricher()/removeEnricher()/setRedactor()for runtime pipeline management - Added
LogController(clock:)for deterministic timestamps in tests -- propagated throughLogScopeandLogSpanHandle
3.2.1 #
Fixes:
- Fixed
LogController.add()redundant enricher condition that was always true - Fixed
LogRedactornot redacting error messages and log message text -- now matchesfield=value/field:valuepatterns in strings - Fixed
ConsoleOutputJSON key escaping -- keys containing",\, newlines are now properly escaped - Fixed
RingBufferOutputusing fakeLogEventplaceholder -- replaced with nullable list - Fixed
RpcLogOutput.flush()silently swallowing send errors -- addeddroppedCountcounter - Removed dead
noSuchMethodoverride from noop span handle - Added
RpcResponderEndpoint.setLogController()for post-construction logger injection
3.2.0 #
Built-in logging system:
- New
LogController/LogScopearchitecture replaces oldRpcLogger - Pipeline: level filter -> sampling -> enrichers -> redaction -> outputs
LogScope.noopfor zero-cost disable when no logger configuredsealed LogRecordwith three types:LogSpanStart,LogEvent,LogSpan- Endpoints accept
LogController? loggerparameter
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 spansEnabledflag 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.login 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 limitingLogEnricher— auto-attach fields (host, pid, etc.) to every recordLogRedactor— strip sensitive fields (password, token) from dataRingBufferOutput— in-memory history, queryable byLogFilter
RPC log transport:
RpcLogOutput— send logs to remote peer with offline bufferRpcLogResponder— accept remote logs into local controllerRpcLogServiceResponder— expose subscribe stream + history + remote controlRpcLogServiceCaller— client API for remote diagnostics
Breaking:
- Removed
RpcLogger,RpcLoggerColors,AnsiColor,RpcLoggerLevel,RpcContextAwareLogger,RpcContextualLogging - Removed extension methods:
logRpcError,logRpcWarning,logStreamBound,logStreamFinished,logMessageReceived - Endpoint constructors:
loggerColorsparameter removed,loggerparameter now takesLogController?
3.1.1 #
Bug fix:
- Fixed
Uint8ListCBOR encoding on dart2js: addedTypedDatafallback in_FastCborWriterand_encodeValueto 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:
RpcStatusExceptionnow carries typeddetailslist — structured error information sent viagrpc-status-details-bintrailer (wire-compatible with standard gRPCgoogle.rpc.Status).- Added
RpcErrorDetailhierarchy: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
RpcStatusExceptioninstead of genericExceptionon gRPC errors. - All responder patterns forward
statusDetailsBinin error trailers when handler throwsRpcStatusExceptionwith details. - Minimal protobuf encoding/decoding for
google.rpc.Statusandgoogle.protobuf.Any— no external protobuf dependency.
Graceful drain:
- Added
RpcResponderEndpoint.drain({Duration timeout})— initiates graceful shutdown: rejects new streams withUNAVAILABLE, cancels active stream contexts via cancellation token, waits for completion. - Responder pipeline now attaches
RpcCancellationTokento all incoming stream contexts automatically — handlers can listen for cancellation during shutdown. RpcApp._drainEndpoints()(framework) now callsendpoint.drain()in parallel instead of polling.
Other:
- Added
RpcPeerContract— base class for bidirectional contracts where either side can initiate calls. ExtendsRpcResponderContractand exposescallUnary,callServerStream,callClientStream,callBidirectionalStreammethods bound to the sameRpcPeerEndpoint. RpcPeerEndpointis now exported from the library public API.RpcServiceKindenum added toannotations.dart(unidirectional/peer) for use with@RpcService(kind: ...).@RpcServicegainedgrpcDescriptorflag (defaultfalse) — whentrue, the generator emits agrpcDescriptorstatic field for gRPC Server Reflection registration.RpcContextUtils.generateTraceId()is now public (was_generateTraceId).RpcResponderContract.serviceNamesupports dynamic suffix viaserviceNameSuffixsetter — used for scoped/isolated service registrations.- Internal refactor:
caller_pipeline.dartandresponder_pipeline.dartextracted as separate part files; logic unchanged.
3.0.1 #
RpcMessageParser: replacedList<int>backing buffer withUint8List— 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 withByteData.setUint64, which is correctly implemented on both Dart VM and dart2js.RpcContext.withAdditionalHeaders: removed duplicate_sanitizeHeaderscall that could silently drop headers when the merged map exceeded the 128-header or 64 KB limit.RpcCallerEndpoint: changedcompressionEnableddefault fromtruetofalseto 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 interfaceIRpcMultiplexedChannel— multiplexed message channel between channel and transportRpcFrameMultiplexedChannel— wrapsIRpcChannelwith 9-byte frame codecRpcDirectMultiplexedChannel— zero-copy in-memory paired channelRpcChannelTransport—IRpcTransportwrapper with.pair(),.memoryPair(),.fromChannel()factoriesRpcInMemoryTransport.pair()is now a thin delegate toRpcChannelTransport.memoryPair()
Resilience primitives (moved from rpc_dart_framework to core):
RpcRetryInterceptor— configurable retry withBackoffPolicy(constant, linear, exponential)RpcCircuitBreakerInterceptor— circuit breaker with open/half-open/closed statesRpcClientConnection— reconnect state machine with health monitoringRpcRateLimiter— pluggable rate limiting with per-key dynamic limits
gRPC Health Checking Protocol:
RpcGrpcHealthService— implementsgrpc.health.v1.Health(Check+Watch)RpcGrpcHealthClient— typed client for health checks
Other:
RpcBinaryCodec<T>now works with anyT extends Object(not justIRpcSerializable)RpcStatusException(statusCode, message)— handlers can return specific gRPC status codesdart:typed_datare-exported viarpc_dart.dart(Uint8Listavailable without extra import)- Removed
onServiceRegisteredcallback fromRpcResponderEndpoint
2.6.3 #
- Added
RpcRemovedannotation. Marks an inherited RPC method as removed in a versioned contract. The generator produces a@Deprecated+throw UnsupportedErrorimplementation 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 singlecompact()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 conditionbuffer.length >= 5prevented 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: removedhostandportfrom the interface — these are transport-level concerns, not RPC server concerns.IRpcServerFactoryinterface removed along withRpcHttp2ServerFactoryandRpcWebSocketServerFactory— the abstraction had no consumers.
2.6.0 #
- Added
RpcHeadersclass with gRPC semantic header name constants; all internal metadata and protocol code now usesRpcHeaders.*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 inRpcMetadata;http2HeadersToRpcMetadatafilters them out automatically. RpcHttpCorsPolicy: always exposesgrpc-encoding,grpc-status,grpc-message,grpc-accept-encodinginAccess-Control-Expose-Headersso browsers do not block them in cross-origin responses.RpcHttpCorsPolicy: always allowsgrpc-timeout,grpc-encoding,grpc-accept-encodinginAccess-Control-Allow-Headers.RpcHttpCorsPolicy:exposedHeadersparameter renamed toextraExposedHeadersto clarify it is additive on top of the required gRPC headers.RpcCallerEndpoint: injectsgrpc-accept-encodinginto context based oncompressionEnabledso 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:
_ensureGrpcFramenormalises parser output to gRPC frames, consistent with HTTP/2 transport delivery toincomingMessages. - Exported
CborCodecfromrpc_dartfor use by transport packages.
2.5.1 #
- Compression is now pluggable via
RpcCompressionCodecinterface andRpcGrpcCompression.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:
RpcCallerEndpointnow has acompressionEnabledflag (defaulttrue) that automatically injectsgrpc-encoding: gzipfor non-zero-copy transports (e.g. network transports). StreamProcessorauto-detects request/response encoding from incoming metadata (grpc-encoding/grpc-accept-encoding), enabling transparent decompression on the server side.UnaryRespondercapturesgrpc-accept-encodingper stream to compress unary responses when the client advertises support.RpcMetadata.forServerInitialResponsenow accepts an optionalencodingparameter to includegrpc-encodingin the initial server response headers for streaming compression.- Added compression tests for all stream types (unary, server streaming, client streaming, bidirectional).
- Fix:
UnaryRespondernow capturesgrpc-encodingfrom incoming client metadata per stream and uses it for decompression, matching the behaviour ofStreamProcessor. - Fix: caller and streaming processors now validate
grpc-encodingagainst supported encodings before compressing and throwRpcExceptionwith a clear message instead ofUnsupportedError. - Fix:
UnaryRespondernow throwsRpcException(was bareException) when the request frame is empty. - Refactor: per-stream state in
UnaryResponderconsolidated from five parallel maps into a single_UnaryStreamStateobject — cleanup is now atomic. - Refactor: response-encoding negotiation logic extracted to
RpcGrpcCompression.selectResponseEncoding()and reused by bothUnaryResponderandStreamProcessor. - Refactor:
RpcCallerEndpoint._effectiveContextrenamed to_prepareContextfor 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
RpcSecurityPolicyto centralize transport/parser limits and metadata validation. - Security:
grpc-messageis now percent-encoded per gRPC HTTP/2 spec; addeddecodeGrpcMessagehelper. - Protocol: client request metadata now includes
grpc-accept-encoding(advertises supported message encodings). - Fix: clean up
RpcCallerEndpointcancellation token registry after completed calls (prevents memory growth in long-lived clients). - Security:
RpcInMemoryTransportnow enforces policy limits (metadata validation, active stream cap);pair()accepts an optionalpolicy.
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
RpcMiddlewareContextenhancements 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
RpcBinaryCodecfor 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 viaRpcEndpointHealthandRpcHealthStatus. - 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()andIRpcTransport.reconnect()plus detailed implementations forRpcInMemoryTransport,RpcTransportRouterand the transport toolkit, including automatic partner shutdown to avoid close deadlocks. - Improved
RpcStreamIdManagerso 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->RpcLoggerRpcContextPropagation->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
internallogging 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
RpcStreamIdManagerfor 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