Add HTTP/2 support (RFC 9113 + HPACK + h2c + ALPN)#4
Open
robotdan wants to merge 201 commits into
Open
Conversation
Three deliverables from the brainstorming session: - docs/specs/HTTP2.md — long-lived RFC 9113 / RFC 7541 compliance matrix + architecture overview + peer comparison vs Jetty/Tomcat/Netty/ Undertow/Helidon. Mirrors HTTP1.1.md in role. - docs/superpowers/specs/2026-05-05-http2-design.md — dated implementation blueprint covering decisions made, class layout, threading model, frame layer, HPACK, stream state machine, flow control, trailers API, h2c-Upgrade handoff, security mitigations, configuration knobs, test plan, and HTTP/1.1 spec drift. - docs/superpowers/specs/2026-05-05-http11-conformance-cleanup-design.md — small parallel sibling spec for HTTP1.1.md drift items found during the brainstorm review (mostly verification-only test gaps, plus one open item: 417 for unknown Expect values). Scope locked during brainstorming: full transport surface (h2 ALPN + h2c prior-knowledge + h2c Upgrade with the 101 hook as a prerequisite), trailers in API for both protocols, gRPC interop as explicit goal, default-on for h2 over TLS, push deliberately out of scope. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Resolve open implementation gaps surfaced during review: ALPN wiring on SSLSocket, per-stream pipe (ArrayBlockingQueue<byte[]>), per-connection frame buffers in HTTPBuffers, ClientConnection interface for cleaner-thread integration, settings retroactive window adjustment coordination, DATA fragmentation against peer MAX_FRAME_SIZE, full RFC 9110 §6.5.2 trailer deny-list enumeration, h2 keep-alive / Connection / Transfer-Encoding / Expect: 100-continue semantics, withHTTP2SettingsAckTimeout knob. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Six task-by-task TDD plans covering the full HTTP/2 delivery: - A: HTTP/1.1 conformance cleanup (Expect 417, verification tests) - B: HTTP/1.1 trailers API + 101 Switching Protocols hook - C: HTTP/2 protocol layer (frame codec, HPACK, state machine, flow control) - D: HTTP/2 wire-up (HTTP2Connection, ProtocolSelector, ALPN, all 3 transport modes, DoS limits) - E: HTTP/2 conformance + interop (h2spec, gRPC streaming patterns) - F: HTTP/2 perf + polish (h2load benchmarks, JFR profiling) Plans A–D are concrete; E–F are deliberately outline-shaped where work depends on discovery output (h2spec failures, JFR hotspots). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
License headers: switch all new-file code samples in plans from Apache-2.0 boilerplate to "Copyright (c) 2026, The Latte Project" per CLAUDE.md convention. Plan B (h1.1 trailers + 101 hook): - Task 6: rewrite trailer parsing to read line-by-line via parseTrailers() helper after the 0\r\n chunk; isolates from the chunk state machine so future refactors can't break trailer capture. - Task 9: HTTPOutputStream takes HTTPRequest in constructor, not via setter — correctness-by-construction (caller can't forget). - Task 10: add explicit pre-task socket-ownership audit step before writing switchProtocols implementation. Plan C (HTTP/2 protocol layer): - Task 4: clean up buffer sizing — single grow-on-demand byte[16384] form, capped at the negotiated MAX_FRAME_SIZE (not RFC ceiling). Drop the 16 MB-up-front version that read as catastrophic. - HPACKDynamicTableTest: add max_size=0 test (peer can advertise this). - HTTP2FlowControlTest: add SETTINGS-induced negative window test; asserts signed >= comparison vs naive > 0. Plan D (HTTP/2 wire-up): - Task 5: add minimal HTTP2Connection stub as Step 0 so ProtocolSelector compiles. Wrap preface peek in try/catch SocketTimeoutException to fall through to h1.1 — fixes slowloris vector. - Task 9: explicit HEADERS+CONTINUATION interleaving check in the reader loop (RFC 9113 §6.10); not implicit in buffer scoping. - Tests: assertEquals(resp.version(), HTTP_2) on every JDK HttpClient h2 test — JDK silently downgrades to h1.1 on ALPN failure. - Task 14: graceful shutdown after GOAWAY bounded by existing configuration.getShutdownDuration(); no new knob. Plan E (conformance + interop): - Make export:false on test deps explicit so zero-dep promise stays visible in project.latte. - Tighten gRPC interop wording: "framing-compatible with grpc-java clients," not "drop-in grpc-java server-side." Spec docs: - HTTP2.md: footnote on Helidon Níma row (Níma uses VTs as carrier threads for an event loop, not strict VT-per-stream); mention withShutdownDuration as the graceful-shutdown bound. - http2-design.md: §"Settings retroactive window adjustment" reinforces signed comparison; new §"HEADERS/CONTINUATION interleaving" calls out the rule explicitly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per .claude/rules/copyright.md: brand-new files use exactly: Copyright (c) 2026 Latte Java SPDX-License-Identifier: MIT Replace earlier "Copyright (c) 2026, The Latte Project" form across all 6 plans. No comma, no "All Rights Reserved", SPDX tag included for machine-readable license detection. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…havior Six raw-socket tests lock in RequestPreambleState behavior that HTTP1.1.md §6 listed as⚠️ "needs test": bare CR in header value, whitespace before colon, obs-fold, chunk-extensions, OPTIONS *, and empty Host — all six pass on first run. Also adds expectResponseSubstring to BaseSocketTest for lenient 200-response assertions. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
assertResponse and assertResponseSubstring shared 38 lines of server-setup and socket I/O; pull into a private sendAndCapture helper so future configuration additions don't need to be applied in two places.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…est inner classes - HTTP1.1.md: Clarify that only OPTIONS * was completed in Plan A; absolute-form and authority-form request-target tests remain. - BaseSocketTest: Move protected Builder class before private ThrowingConsumer interface per code-conventions.md visibility ordering (public → protected → package → private). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add ForbiddenTrailers constant set with lowercased header names forbidden in trailers (framing, routing, auth, request modifiers, response control, caching, connection management). - Add TE and Trailer header-name constants to Headers inner class. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Match the existing pattern used by addHeader/getHeader.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
On first EOF from the chunked delegate, copy all parsed trailers onto the HTTPRequest so handlers can read them via req.getTrailerMap(). A separate chunkedDelegate reference is kept so the copy works even when a compression wrapper (GZIPInputStream/InflaterInputStream) sits in front of ChunkedInputStream. The trailersCopied guard prevents redundant copies on repeated post-EOF reads. No change to BaseSocketTest was needed — Builder.withHandler() already existed and was used directly in the new RequestTrailersTest. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Force chunked framing, auto-set Trailer: header, gate emission on TE: trailers (RFC 9110 §6.5). HTTPOutputStream takes HTTPRequest in constructor for correctness-by-construction. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ocation Adds switchProtocols(protocol, additionalHeaders, handler) to HTTPResponse that records protocol-switch intent. HTTPWorker.run() checks isProtocolSwitchPending() after the handler returns, emits the 101 Switching Protocols preamble directly to the socket, then hands the socket to the ProtocolSwitchHandler and returns — leaving the socket open and owned by the handler. Normal response.close() / keep-alive logic is bypassed for this path. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tion/Upgrade overrides Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ForbiddenTrailers was incorrectly positioned after ControlBytes, breaking alphabetical ordering of nested classes. Moved it to its correct position after DispositionParameters and before HeaderBytes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Rewrite HTTPOutputStream into a protocol-agnostic response body pipeline shared by HTTP/1 and HTTP/2 via a two-method output seam, giving HTTP/2 full gzip/deflate parity and deleting HTTPResponse.rawOutputStream. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ession Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…eaming compression
The protocol-seam rewrite dropped the Javadoc on isCommitted(), setSuppressBody(), and willCompress() and left the class doc stale. Restore them with current behavior, document the remaining public methods (close/isCompress/setCompress/reset), and update the class doc to describe the shared HTTP/1.1 + HTTP/2 pipeline. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The teardown finally has a receive-buffer drain whose purpose is to keep close() from emitting a TCP RST when the peer has unread bytes in the OS receive buffer. It was gated on `if (socketIn != null)`, but socketIn was declared = null and never assigned: a refactor that moved input-stream creation into ProtocolSelector dropped the original `socketIn = in` assignment (present in 2829cc4) and orphaned the declaration + drain block. The drain has been dead code ever since, so every h2 teardown closed with unread data and the OS emitted RST instead of FIN. Drain the inputStream field (exactly what socketIn used to hold). Fixes the intermittent RST in HTTP2IdleStreamErrorsTest.invalid_preface_response_ completes_cleanly_with_goaway_not_rst (0/20 failures, was ~25-50%). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
On server.close() the acceptor calls connection.shutdown() (which only enqueues a GOAWAY on the async writer queue) and then interrupts the connection's virtual thread. Interrupting a virtual thread blocked in a socket read closes the socket (JDK virtual-thread I/O cancellation), so the interrupt races the writer's flush of the GOAWAY. When the interrupt wins the peer never sees the GOAWAY, which made HTTP2GoawayTest.goaway_on_graceful_ shutdown intermittently fail (~10%). shutdown() now enqueues the GOAWAY plus the writer-shutdown sentinel and joins the writer thread (captured as a field) with a 1s bound. The writer processes its queue in order — GOAWAY first, flushed, then the sentinel — and exits, so by the time shutdown() returns (before the acceptor's interrupt) the GOAWAY is on the wire. Deterministic, no sleep. HTTP2GoawayTest 30/30; full suite 2941/2941 (both previously-flaky h2 timing tests now green). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nts to HTTPValues from the private HTTP1Connection class
…gs class rather than the HTTP2Connection class.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Introduce HTTP2WriterThread (the outbound queue + virtual thread + drain loop, owning the closed flag and the enqueue policies) and rewrite the coalescing unit test to drive its run() directly. HTTP2Connection still uses its own writer code until the next task wires it through. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
HTTP2Connection now owns an HTTP2WriterThread instead of a raw queue + thread handle + dead flag + inline loop. HTTP2OutputStream takes the writer object and enqueues via enqueueBlocking. Deletes runWriterLoop, enqueueForWriter, writerQueue, writerThread, writerDead, and WRITER_BATCH_SIZE from HTTP2Connection. No wire-behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…eld shadow Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… to frame writer.
Separate server configuration into shared options on Configurable plus HTTP1Configuration and HTTP2Configuration sub-configs accessed via Consumer lambdas. Drop HTTP/2 knobs that duplicate shared config (maxHeaderListSize collapses into maxRequestHeaderSize) and the unwired keep-alive ping and SETTINGS-ACK timeouts. Convert HTTP2RateLimits from record to builder class. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nobs Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e-size upper-bound test, test rename) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds HTTP/2 support to the latte-java/http server: RFC 9113 frame codec, HPACK encoder/decoder, h2c (prior-knowledge and Upgrade), ALPN-negotiated h2 over TLS, gRPC interop, and h2spec conformance harness. Forked from FusionAuth/java-http; this is the largest single feature drop on the project so far.
Highlights
Wire protocol
Transport
ProtocolSelector.withH2cPriorKnowledgeEnabled/withH2cUpgradeEnabledand the existing TLS cert/key wiring.Conformance + interop
H2SpecHarnessTestand documented indocs/specs/HTTP2.mdbug ledger).HTTP2HeaderValidationTestcovering RFC 9113 §8.1.2.*.Security
HTTP2RateLimits+ per-connection tracker.Benchmarks
h2-hello,h2-compute,h2-io,h2-stream,h2-large-response,h2-connection-concurrency, TLS+h2 variants.docs/BENCHMARKS.mdanddocs/specs/HTTP2.mdperformance findings.Pre-PR review fixes (last 12 commits:
e131c22..e56a297)HTTPInputStream(skipFixedLengthInputStreamon h2 so EOF comes from END_STREAM, not byte count).Plan Eplaceholder rejection).GOAWAY(INTERNAL_ERROR)instead of bare TCP FIN.writerDeadflag +enqueueForWriterhelper (replaces blockingwriterQueue.puton the reader side).pipe.offer(timeout)+RST_STREAM(CANCEL), configurable viawithHTTP2HandlerReadTimeout.GOAWAY(COMPRESSION_ERROR)per RFC 7541 §2.1 (was unchecked exception escaping as INTERNAL_ERROR).RST_STREAM(PROTOCOL_ERROR)per RFC 9113 §8.1.2.6.synchronized(stream)around state check +applyEvent+ enqueue.RST_STREAMon already-closed stream tolerated (rapid-reset path).HTTPInputStream.drain().HTTPServerThreadwhenProtocolSelector.select()throws.H2SpecHarnessTestto surface drift.Scope and limits
docs/specs/HTTP3.md.PUSH_PROMISEoutbound) intentionally not implemented (deprecated by browsers; spec calls it out).400 Bad Requestuntil Plan E lands.Test plan
latte clean int --excludePerformance --excludeTimeouts— 2903 / 2903 pass.int-h2spectarget) — 143/147 pass; the 4 known failures (§6.5.3, §6.9.1, §6.9.2) are pinned inH2SpecHarnessTest.KNOWN_FAILING_SECTIONSso silent drift fails the test.docs/BENCHMARKS.mdanddocs/specs/HTTP2.md(2026-05-21 perf findings).Notes for reviewers
docs/superpowers/plans/2026-05-21-http2-pre-pr-fixes.mdif useful for context.HTTPInputStream.java); new files use MIT/SPDX per.claude/rules/copyright.md.🤖 Generated with Claude Code