2.9 KiB
HTTP/2 streams and request dispatch
Each connection owns a fixed-capacity Http2StreamTable. Client stream identifiers are validated
as odd, non-zero and strictly increasing before a stream object is acquired. The table uses
primitive open addressing and a bounded object free list; it never grows beyond the advertised 64
concurrent streams. An excess request receives REFUSED_STREAM, allowing the peer to retry it.
State model
Http2StreamState represents IDLE, OPEN, HALF_CLOSED_REMOTE, HALF_CLOSED_LOCAL and
CLOSED. A class-initialized table maps every receive/send event to either its next state or the
correct stream error. Frames racing with a recently closed stream follow RFC 9113 §5.1 rather than
being rejected uniformly.
Header and request model
Decoded HPACK fields are copied into storage owned by the stream. Before dispatch,
PseudoHeaders enforces ordering, uniqueness, required request pseudo-fields, lowercase regular
names, connection-specific-field rejection, the te: trailers exception and host/authority
consistency. Pseudo-fields are not exposed as regular headers; :authority is also visible as
host so existing middleware sees the same authority through HTTP/1.1 and HTTP/2.
The stream assembles the existing protocol-neutral Request, RequestLine, RequestBody and
HeaderView models. Path/query splitting, routing, middleware, not-found handling and exception
handling therefore use the same code as HTTP/1.1. FastPathRouterImpl is unchanged.
Dispatch and ownership
The connection thread decodes and validates frames only. Completed bodyless streams are queued in a fixed array while more frame bytes are already buffered, then submitted to the server's shared virtual-thread executor before the demultiplexer waits for the network again. This preserves burst admission semantics without adding a dispatch timer or blocking the connection thread.
The stream owns its pooled request, response, body, decoded-header arena and response writer. Normal response completion releases it through the serialized writer callback. RST_STREAM marks a queued or running stream cancelled and defers release to that sole owner; setup, routing and handler failures send an appropriate stream reset and release in the failure path. A 100,000-cycle test proves stable pool counts, and an immediate request/reset/request regression test covers reuse while dispatch is pending.
Verification
- Clean Maven build with JMH sources: 618 tests, no failures.
- h2spec sections 5 and 8: 37/39. The two remaining cases require request DATA byte accounting and are completed with body flow control.
- Java
HttpClientnegotiates HTTP/2 over TLS and runs an existing parameterized route unchanged. - curl prior-knowledge h2c receives a valid
200response and body. - JMH pooled lifecycle (HPACK decode, request assembly, response write and release): 458.499 ns/op, 0.003 B/op, no GC.