Building a Leak-Safe gRPC Frame Decoder on Reactor Netty

2026年8月8日1 次浏览来源:Dev.to阅读原文

This is the second article in my grpc-reactor series.

The first article explains why I chose to build the runtime directly on Reactor Netty and where its compatibility boundary sits.

This article moves one layer down into the Stage 1 protocol implementation: the frame decoder that every RPC shape relies on. gRPC protobuf messages are not written directly as raw bytes into HTTP/2 DATA frames.

Every message starts with a five-byte envelope: Encoding this envelope is straightforward.

The difficult part is decoding it without assuming that one input buffer contains one complete frame.

HTTP/2, TCP, and Reactor Netty do not promise that buffer boundaries will line up with gRPC message boundaries.

This post describes the Stage 1 protocol layer.

The project has since progressed beyond it, but the ownership and bounded-decoding rules introduced here remain the foundation for the later transport stages.

Encoding Must Define Ownership The contract of is deliberately explicit: the returned frame and the input message have independent lifetimes.

Encoding must not move the input reader index or release the input buffer.

The implementation currently copies the readable bytes into a byte array before applying compression: This is not a zero-copy implementation, and it should not be presented as the fastest possible design.

The copy makes the ownership boundary easy to reason about first.

If a later optimization uses a slice or a composite buffer, cancellation and exception paths must be re-proven instead of assuming that the old ownership rules still hold.

The Decoder Is a Per-Subscription State Machine uses so every subscription receives an independent decoder instance: The state is intentionally small: either the five-byte header is incomplete, or the header is complete and the decoder is collecting a payload of a known length.

One input may contain one byte of a header, or three complete messages back-to-back. preserves source order and limits the number of source buffers being processed at once.

The decoder still has to respect downstream demand when it emits decoded messages.

Every source buffer is released in , including success, failure, and cancellation: The complete implementation also tracks an explicit per-stream buffered-byte limit.

That limit covers an incomplete header, a partial payload, and bytes still present in the current source buffer.

Validate Peer Input Before Allocating The length field comes from the peer, so it must be validated before allocating a payload array.

The decoder first combines the unsigned big-endian bytes in a , then checks the wire-size limit: Wire size and decompressed size are separate limits.

A tiny gzip payload can expand into a huge message, so gzip decompression must enforce a second output limit to defend against decompression bombs.

Only the lowest compression-flag bit is valid.

Any reserved bit is a protocol error.

A compressed frame received while the negotiated codec is still is also rejected; the decoder must not guess which algorithm the peer intended.

Test Every Header Split Point Testing one arbitrary two-buffer split is not enough.

A five-byte header has six representative split positions, including before the first byte and after the complete header.

The test suite uses a dynamic test for every split from 0 through 5: The same test class covers arbitrary body fragmentation, multiple messages coalesced into one buffer, empty messages, gzip, reserved flags, truncated frames, wire/decompressed limits, and the fact that encoding does not consume the input buffer.

See for the executable cases.

Cancellation Must Release Undelivered Data Suppose one source contains three messages: , , and .

The downstream requests two messages and then cancels.

The test must assert not only the values it received, but also that the source buffer was released: That assertion is more important than a happy-path content check.

Network code often behaves correctly under normal completion; leaks tend to appear during cancellation, size-limit failures, truncated frames, or competing terminal signals.

Cancellation can also arrive before a complete message exists.

In that case there is no decoded value for the subscriber to release, so the decoder itself must release the partially accumulated source buffer: The full executable case is .

It covers the lifecycle edge that a normal decode-complete test cannot exercise.

Run only the frame codec suite from the repository root: On JDK 25, the Gradle build and generated JUnit report produced: At the Stage 1 boundary, the frame decoder verifies message-level demand but does not yet implement the two-level flow-control problem of the streaming transport.

Reactive Streams counts messages, while HTTP/2 flow control counts bytes.

They cannot be treated as the same quantity.

Stage 3 later adds bounded inbound buffering and demand-aware delivery, and Stage 4 extends those rules to bidirectional streaming.

Those transport and stress tests are covered in later posts.

Metadata: Ordering, Duplicates, and Binary Values gRPC metadata is not a simple .

It must satisfy all of these rules: The same key may occur more than once, and insertion order matters.

Keys may contain only lowercase letters, digits, , , and , validated by .

Keys ending in carry binary values and use unpadded Base64 on the wire.

Applications cannot set reserved fields such as , , , or . stores an immutable entry list so it can be safely shared across asynchronous boundaries: When parsing HTTP/2 headers, a binary value may be comma-joined by header handling.

The implementation splits it on commas and decodes each Base64 segment independently.

The total encoded size is bounded at 8 KiB by default, preventing a peer from exhausting memory with oversized headers.

Status: 17 Codes and Percent-Encoding gRPC defines 17 standard status codes, each with a specific meaning for client error handling and future retry policies. is a record containing a code and a human-readable message: Several details matter in practice: may be returned even after the operation completed successfully.

If the successful response crosses the deadline in transit, the client can still observe a timeout. indicates a transient failure for which a client may later retry safely; generally describes a server-side bug and should not be blindly retried. carries the semantic meaning of an unsupported method, commonly surfaced through an HTTP 404 response at the protocol boundary.

Text in the trailer uses percent-encoding: printable ASCII characters other than can pass through, while other bytes become .

This allows UTF-8 error descriptions to travel through ASCII HTTP/2 headers safely.

Unknown numeric status codes are mapped to instead of causing a parse failure.

That preserves forward compatibility when a peer adopts a newer gRPC specification.

Timeout: Eight Digits and a Unit The gRPC header carries a relative duration, not an absolute timestamp.

By the time the server receives a request, part of the caller's original time budget has already been consumed by transport latency.

The wire format is compact: at most eight decimal digits followed by a unit suffix: The six units are (hours), (minutes), (seconds), (milliseconds), (microseconds), and (nanoseconds).

When formatting a , the implementation rounds upward using ceiling division.

The encoded deadline must never be shorter than the caller's requested duration: avoids overflow during nanosecond arithmetic.

The formatter scans from nanoseconds upward and selects the first unit whose value fits within 99,999,999.

Compression: Identity by Default, Gzip Built In manages codec registration and negotiation: is always implicit and appears first in the registry.

The codec interface has only three operations: a name, byte-array compression, and bounded byte-array decompression.

Gzip decompression reads in 8 KiB chunks and uses while accumulating the output size.

It can stop immediately after exceeding , and arithmetic overflow cannot sil

分享
Baike.dev

baike.dev helps you discover great languages, frameworks, databases, DevOps and cloud-native tools.

Quick links

About

Contribute

Found a great developer tool? Share it with the community.

Submit a tool
© 2026 baike.dev Developer EncyclopediaUpdated daily · Discover great developer tools