Repository navigation
feat(client): multiplex HTTP/2 requests and clear connections - #13
Merged
Merged
Conversation
takeokunn
force-pushed
the
takeokunn-http-stack-phase5-p2
branch
from
October 23, 2026 17:53
5560097 to
071ee76
Compare
takeokunn
added a commit
that referenced
this pull request
Oct 23, 2026
The child-merge test kept three decoded payload trees and their deep copies live while building canonical inputs. Main and PR #13 CI exhausted the SBCL heap in `accepted-payload`, before `merge-pending` ran. Build canonical inputs one ticket at a time and retain the payload-integrity and exact native-union checks.
takeokunn
force-pushed
the
takeokunn-http-stack-phase5-p2
branch
from
October 23, 2026 19:34
73173fc to
c6ca298
Compare
takeokunn
added a commit
that referenced
this pull request
Oct 24, 2026
The child-merge test kept three decoded payload trees and their deep copies live while building canonical inputs. Main and PR #13 CI exhausted the SBCL heap in `accepted-payload`, before `merge-pending` ran. Build canonical inputs one ticket at a time and retain the payload-integrity and exact native-union checks.
takeokunn
force-pushed
the
takeokunn-http-stack-phase5-p2
branch
from
October 24, 2026 03:19
44c4300 to
9d0d37f
Compare
The successful END_STREAM assertion now expects three connection-level WINDOW_UPDATE frames instead of four.
The successful response path returns stream and connection credit for earlier DATA, then connection credit only for END_STREAM DATA.
The scheduler reports the unexpected stream ID directly, so the public boundary test expects 3.
On SBCL, `%h2-scheduler-open-entry` waits for peer capacity before creating the next request's entry. The zero-capacity test completes stream 1, then receives DATA on stream 3 while the second request is waiting. No request stream is active, so `%h2-connection-wait-for-stream-capacity` is the matching path and reports `(frame-type stream-id)`. eed5954 changed only this test assertion from `(0 3)` to `3`. Keep `(0 3)` for this state. When any scheduler entry without an error remains, preserve scalar stream-ID detail for unknown DATA, as in the existing batch path. The non-SBCL path in `ping-http2-connection` signals `http-protocol-error` with operation `:http2-client` and detail `:eof` when the peer closes while `ping-http2-connection` awaits an ACK. On SBCL, requests and PING share the scheduler reader, which may record EOF while a PING call waits. Store that reader failure on the pending PING waiter and propagate EOF as the same public protocol error. If the PING write fails before the reader records EOF, propagate the write error.
The direct PING path reports unexpected frames with :http2-client and the frame as its detail. When a PING is pending with no non-error request entry, the scheduler must preserve that behavior; otherwise DATA on a retired stream is discarded and the PING call reports the later EOF instead of the DATA-frame error. Keep draining retired DATA while a non-error request entry remains active.
The child-merge test kept three decoded payload trees and their deep copies live while building canonical inputs. Main and PR #13 CI exhausted the SBCL heap in `accepted-payload`, before `merge-pending` ran. Build canonical inputs one ticket at a time and retain the payload-integrity and exact native-union checks.
The #-SBCL ping-http2-connection path reports a malformed PING received while waiting for its acknowledgement as :http2-client with the received frame as detail. The #+SBCL scheduler instead reported :http2-control and a (stream-id payload-length) summary for the malformed frame. Keep :http2-control and summary detail for malformed PING frames without a pending waiter. While a PING waiter is pending, report :http2-client and the received frame as detail. Document the PING ACK-wait error and the existing unknown-DATA detail distinction in the API reference and changelog.
RFC 9113 Section 5.1 closes a stream after both endpoints send END_STREAM and forbids sending frames other than PRIORITY on a closed stream. Section 6.9.1 defines separate stream and connection flow-control windows. These bodyless GET requests end their request streams in HEADERS. Both responses in http2-public-connection-reuses-session end in DATA: the first uses h2-response-wire, which sets END_STREAM on nonempty DATA, and the second DATA frame has flags 9 (END_STREAM and PADDED). Once each response ends, the stream is closed, so only connection-level flow credit is returned. Return connection credit for every DATA frame so other streams can continue. Return stream credit only before response END_STREAM. For scheduler-delivered body callbacks, carry END_STREAM in the queued event so terminal DATA returns connection credit only. http2-public-single-blocked-upload-keeps-response exercises that callback path; http2-sends-window-updates-after-data verifies stream credit for nonterminal DATA.
RFC 9113 Section 5.1 bars frames other than PRIORITY on a closed stream, and Section 6.9.1 defines separate stream and connection flow-control windows. Both bodyless GET request streams end in HEADERS. In http2-public-connection-reuses-session, both responses end with DATA: h2-response-wire sets END_STREAM on the first nonempty DATA frame, and the second DATA frame has flags 9 (END_STREAM and PADDED). Once each response ends, the stream is closed. Expect one connection-level WINDOW_UPDATE per terminal frame and no extra frames after the second update. http2-sends-window-updates-after-data checks stream-level credit for nonterminal DATA.
RFC 9113 Section 5.1 prohibits frames other than PRIORITY after a stream is closed; Section 6.9.1 defines distinct stream and connection flow-control windows. This test processes four response DATA frames: three are nonterminal and stream 3 second DATA frame ends that stream. The HTTP/2 transport returns connection credit for each DATA frame and stream credit only before that stream is closed. Assert four stream-0 updates, two stream-1 updates, and one stream-3 update, omitting stream credit on the closed stream.
In `http2-public-gap-producer-chunks`, `t/support.lisp:114-120` builds nonempty response DATA with END_STREAM, putting the client stream in half-closed (remote). RFC 9113 §5.1 (https://www.rfc-editor.org/rfc/rfc9113.html#section-5.1) does not require maintaining a receive window in that state, and §6.9.1 (https://www.rfc-editor.org/rfc/rfc9113.html#section-6.9.1) distinguishes stream and connection windows. The test expects the client behavior in `http2/transport-read-response.lisp:141-149`: after terminal DATA, only connection credit is restored, with a connection-level WINDOW_UPDATE on stream 0 and no stream-level update. In the expected frame list, type 8 is WINDOW_UPDATE; the final type-8 frame remains on stream 0, while the removed type-8 frame was on stream 1.
The successful case at `t/tests-http2-open-stream.lisp:2865-2868` feeds a response wire sequence to the client: HEADERS with PADDED and PRIORITY flags, then padded DATA with flags 9 (PADDED and END_STREAM). The client request is a bodyless GET. `http2/transport-request.lisp:255-260` passes the empty-body condition to the header encoder, which sets END_STREAM on the request HEADERS (`http2/transport-headers.lisp:156-168`). The stream is therefore half-closed (local) before the response; receiving response END_STREAM transitions it to closed (RFC 9113 §5.1, https://www.rfc-editor.org/rfc/rfc9113.html#section-5.1). Section 6.9.1 (https://www.rfc-editor.org/rfc/rfc9113.html#section-6.9.1) distinguishes stream and connection windows. `http2/transport-read-response.lisp:141-149` restores connection credit and skips stream credit after terminal DATA. Keep the type-8 WINDOW_UPDATE on stream 0 in the successful-case expectation and remove the stream-1 update.
The test's callback runs during the request upload's wait for WINDOW_UPDATE credit. At each callback, the old checks required all scripted peer frames, including the trailing WINDOW_UPDATE frames, to have been consumed and all 65,536 request bytes to have been sent. In the fixture, response callbacks arrive before the trailing WINDOW_UPDATE frames let the last request byte be sent. Assert that each callback occurs before all scripted peer frames are consumed, and that the sent bytes form a prefix of the request body and are fewer than its full length. Existing post-completion checks continue to verify the full upload and DATA frame layout. RFC 9113 §5.1 permits the local endpoint to keep sending after the peer has half-closed its side of the stream; §§6.9 and 6.9.1 define WINDOW_UPDATE credit and the connection/stream windows. The scheduler queues a callback for each response DATA frame at http2/transport-connection.lisp:1717-1743 and runs queued callbacks while upload waits for credit at http2/transport-connection.lisp:1910-1917. The corrected callback checks and retained post-completion checks are at t/tests-http2-response-boundaries.lisp:454-480. References: - https://www.rfc-editor.org/rfc/rfc9113.html#section-5.1 - https://www.rfc-editor.org/rfc/rfc9113.html#section-6.9 - https://www.rfc-editor.org/rfc/rfc9113.html#section-6.9.1
Share a retained HTTP/2 connection across concurrent high-level single-request calls with the same key. Keep batch calls exclusive. After a batch writes request HEADERS, record each stream ID in scheduler state. When the batch reader consumes initial peer SETTINGS, mark the shared session started and clear the first-frame guard. A request refused after GOAWAY retires the connection once no active streams remain. Clamp per-request header and body limits to connection caps. Document and test connection-wide response-dispatch failures; RFC 9113 §5.4.1 permits an endpoint to treat a stream error as a connection error. Keep max-connections as a retained-entry limit because the manager does not define whether calls wait or fail at a total-connection cap. The terminal-DATA test expects connection-level credit only. RFC 9113 §5.1 allows an endpoint to stop maintaining the receive window for a half-closed (remote) stream, while §6.9.1 distinguishes stream and connection flow control. The scheduler returns connection credit after END_STREAM at http2/transport-connection.lisp:1751. The native-client test counts one TLS connection for the first two requests, one after client clear, and one after pool clear. Its final overlapping pair reuses the last connection, so the total is three. This follows the same-key multiplexing contract and default origin key in docs/src/reference/api.md:624-626, 632.
A concurrent request can register the shared connection between the initial lookup and the opening claim. Recheck the manager before invoking the open callback so the caller leases that entry.
Keep the client path covered when concurrent first requests race while the HTTP/2 session is being opened.
Cover a close after PING bytes are written but before the write returns, late callback delivery, and scheduler error and closed-state branches.
Add cases for serial fallback reuse, peer-reset session reuse, control-frame handling, retired-stream dispatch, and scheduler callback delivery, flow-control updates, and retirement.
takeokunn
force-pushed
the
takeokunn-http-stack-phase5-p2
branch
from
October 24, 2026 19:38
dd67ff0 to
fc8ca5c
Compare
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.
Concurrent HTTP/2 calls share a per-connection scheduler that serializes stream admission and connection-wide state while response streams progress independently. Clearing the client pool invokes the HTTP/2 manager's clear callback. The GOAWAY boundary and draining transition are captured under the same scheduler lock used for stream admission, so a racing request is either included in GOAWAY's boundary or rejected before HEADERS are sent; subsequent calls open a fresh connection. Tests cover active-stream clearing, overlapping requests, and the GOAWAY/open race.