Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
76c27df
feat(http2): add concurrent client scheduling and connection clearing
takeokunn Oct 21, 2026
19475be
fix(http2): serialize GOAWAY with stream admission
takeokunn Oct 22, 2026
a0494ee
fix(client): restore pool clear behavior
takeokunn Oct 22, 2026
33c3a89
fix(http2): retire rejected GOAWAY connections
takeokunn Oct 22, 2026
42d2d7f
test(http2): assert response body callback counts on read errors
takeokunn Oct 22, 2026
97bccbe
test(http2): align callback cleanup frame expectations
takeokunn Oct 22, 2026
37f1a5d
test(http2): correct callback credit stream targets
takeokunn Oct 23, 2026
1b08324
test(http2): correct scheduler error detail expectation
takeokunn Oct 23, 2026
ad03a95
fix(http2): preserve protocol error behavior in scheduler
takeokunn Oct 23, 2026
1b78798
fix(http2): surface unexpected DATA during PING wait
takeokunn Oct 23, 2026
1d40e13
test(coverage): bound child payload retention
takeokunn Oct 23, 2026
263924e
fix(http2): align scheduler error details
takeokunn Oct 23, 2026
9733720
fix(http2): skip stream credit after END_STREAM
takeokunn Oct 23, 2026
b56cf16
test(http2): expect connection credit after terminal data
takeokunn Oct 23, 2026
6759bc6
test(http2): expect per-stream credit for nonterminal data
takeokunn Oct 23, 2026
002a703
test(http2): expect connection WINDOW_UPDATE after terminal data
takeokunn Oct 23, 2026
cde40ff
test(http2): expect connection update after padded terminal data
takeokunn Oct 23, 2026
b1c6a17
test(http2): check response chunks during blocked upload
takeokunn Oct 23, 2026
a96d6d9
fix(http2): multiplex concurrent client calls
takeokunn Oct 23, 2026
9471784
fix(http2): reuse a connection after winning the cold-open claim
takeokunn Oct 24, 2026
6290a0e
test(http2): verify cold-open sharing through the client
takeokunn Oct 24, 2026
d35513a
test(http2): cover scheduler close and callback boundaries
takeokunn Oct 24, 2026
a9266b0
test(http2): cover serial fallback and scheduler boundaries
takeokunn Oct 24, 2026
1dd218e
test(http2): cover scheduler and manager boundary paths
takeokunn Oct 24, 2026
8fbdfef
refactor(http2): evaluate cancel error default in callback body
takeokunn Oct 24, 2026
70d7141
test(http2): bypass the SBCL package lock when wrapping condition-wait
takeokunn Oct 24, 2026
0a32f47
refactor(http2): isolate wait-lock recovery for direct testing
takeokunn Oct 24, 2026
5b84bbf
test(http2): cover buffered initial response frames
takeokunn Oct 24, 2026
440e19f
test(http2): cover exclusive manager entry branch
takeokunn Oct 24, 2026
29557eb
test(http2): respond to nested request promptly
takeokunn Oct 24, 2026
0c5e4f1
fix(client): remove duplicate connection clear export
takeokunn Oct 24, 2026
c2dcbd8
test(http2): assert GOAWAY waits on scheduler lock
takeokunn Oct 24, 2026
23df556
fix(http2): bound informational callback queue
takeokunn Oct 24, 2026
6e2e1f9
fix(http2): retire streams after local cancellation
takeokunn Oct 24, 2026
8e37d4e
test(http2): block upload data after peer reset
takeokunn Oct 24, 2026
345a79d
fix(http2): preserve response after no-error reset
takeokunn Oct 24, 2026
4494b96
fix(http2): honor reduced peer frame size
takeokunn Oct 24, 2026
2ca90ad
fix(test): balance scheduler cancellation handlers
takeokunn Oct 24, 2026
19363d6
fix(test): balance informational queue handlers
takeokunn Oct 24, 2026
46142f6
test(http2): defer frame-size shrink until body is produced
takeokunn Oct 24, 2026
38512ef
test(http2): scope frame shrink to body producer
takeokunn Oct 24, 2026
fc8ca5c
test(http2): report frame shrink request causes
takeokunn Oct 24, 2026
6d313d9
test(http2): pass frame object to settings handler
takeokunn Oct 24, 2026
a74f2ee
test(http2): cover remaining upload branches
takeokunn Oct 24, 2026
19b9026
fix(http2): preserve local cancel during flow-control wait
takeokunn Oct 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 38 additions & 30 deletions client/connection.lisp
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
max-connection-age
clock-function
lock
(generation 0)
(clear-callbacks nil)
(entries nil))

Expand Down Expand Up @@ -227,19 +228,22 @@ be used concurrently from multiple threads."
(remove oldest entries :test #'eq))
(%pool-close pool (%http-pooled-connection-stream oldest))))))

(defun %pool-retain (pool key stream opened-at last-used)
(defun %pool-retain (pool key stream opened-at last-used generation)
(let ((max-idle (%http-connection-pool-max-idle pool)))
(if (zerop max-idle)
(%pool-close pool stream)
(progn
(when (>= (length (%http-connection-pool-entries pool)) max-idle)
(%pool-evict-oldest pool))
(push (%make-http-pooled-connection
:key key
:stream stream
:opened-at opened-at
:last-used last-used)
(%http-connection-pool-entries pool)))))
(cond
((/= generation (%http-connection-pool-generation pool))
(%pool-close pool stream))
((zerop max-idle)
(%pool-close pool stream))
(t
(when (>= (length (%http-connection-pool-entries pool)) max-idle)
(%pool-evict-oldest pool))
(push (%make-http-pooled-connection
:key key
:stream stream
:opened-at opened-at
:last-used last-used)
(%http-connection-pool-entries pool)))))
pool)

(defun %pool-open (pool request &key timeout deadline proxy-plan proxy)
Expand Down Expand Up @@ -270,12 +274,13 @@ be used concurrently from multiple threads."
(%http-connection-pool-max-connection-age pool))))

(defun http-connection-pool-clear (pool)
"Close and remove every idle connection in POOL, returning its count."
"Close idle entries in POOL and invoke its clear callbacks. Return the number of idle entries removed."
(%pool-ensure-pool pool)
(let ((entries nil)
(callbacks nil)
(callback-error nil))
(%with-client-lock ((%http-connection-pool-lock pool))
(incf (%http-connection-pool-generation pool))
(setf entries (%http-connection-pool-entries pool)
callbacks (copy-list
(%http-connection-pool-clear-callbacks pool))
Expand Down Expand Up @@ -315,17 +320,19 @@ be used concurrently from multiple threads."
:cause condition))))
(stream nil)
(opened-at nil)
(generation nil)
(retained-p nil))
(handler-case
(with-http-deadline
(absolute-deadline timeout
:inherited deadline
:clock-function #'%pool-monotonic-time)
(let ((entry
(unless *client-preopened-http-stream*
(%with-client-lock ((%http-connection-pool-lock pool))
(%pool-purge-expired pool)
(%pool-take pool key)))))
(%with-client-lock ((%http-connection-pool-lock pool))
(setf generation (%http-connection-pool-generation pool))
(unless *client-preopened-http-stream*
(%pool-purge-expired pool)
(%pool-take pool key)))))
(cond
(*client-preopened-http-stream*
(setf stream (prog1 *client-preopened-http-stream*
Expand All @@ -349,7 +356,7 @@ be used concurrently from multiple threads."
(error '%http-client-alpn-handoff
:stream opened-stream))
((or (null negotiated-alpn)
(equal negotiated-alpn "http/1.1"))
(equal negotiated-alpn "http/1.1"))
nil)
(t
(%pool-protocol-error
Expand All @@ -376,24 +383,25 @@ be used concurrently from multiple threads."
(if reusable-p
(progn
(%with-client-lock ((%http-connection-pool-lock pool))
(%pool-retain pool key stream opened-at (%pool-now pool)))
(%pool-retain pool key stream opened-at (%pool-now pool)
generation))
(setf retained-p t))
(%pool-close pool stream))
(%pool-close pool stream))
(setf stream nil)
response)))
(%http-client-alpn-handoff (condition)
(error condition))
(error condition))
(http-error (condition)
(unless retained-p
(%pool-close pool stream))
(error condition))
(unless retained-p
(%pool-close pool stream))
(error condition))
(error (condition)
(unless retained-p
(%pool-close pool stream))
(error 'http-connection-error
:message "The pooled HTTP exchange signaled an error."
:operation :transport
:cause condition)))))
(unless retained-p
(%pool-close pool stream))
(error 'http-connection-error
:message "The pooled HTTP exchange signaled an error."
:operation :transport
:cause condition)))))

(defun http-connection-pool-send
(pool request &key key timeout deadline max-header-bytes max-fields max-body-bytes
Expand Down
1 change: 1 addition & 0 deletions client/data.lisp
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,7 @@ character in that range is a decimal digit, apart from an optional sign when
;; plus :ALTERNATIVE-SERVICE.
http3-transport-function
connection-pool
clear-connections-function
(default-headers nil)
cookie-jar
cookie-partition-key
Expand Down
9 changes: 9 additions & 0 deletions docs/src/project/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@ historical release behavior, not the current API contract; see the
See the
[WebSocket and SSE migration guide](migration.md#websocket-and-sse-api-migration).

### HTTP/2

- report malformed PING frames received while awaiting an acknowledgement as
`http-protocol-error` with `:http2-client` operation and the received frame
as detail.
- document `:http2-read` detail for DATA on a never-opened HTTP/2 client stream:
the stream ID while another request is in progress and has not failed,
otherwise a list with `0` (the DATA frame type) followed by the stream ID.

## 0.4.1

- require certificate-chain, hostname, and trust verification by default on
Expand Down
41 changes: 32 additions & 9 deletions docs/src/reference/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,10 @@ The condition type for malformed or contradictory protocol data.
### `http-protocol-error-detail`

Return structured protocol detail.
For `:http2-read` errors caused by DATA on a client stream that has never been
opened, the detail is the stream ID when another request on the connection is
still in progress and has not failed; otherwise it is a two-element list with
`0` (the DATA frame type) first and the stream ID second.

### `http-invalid-uri`

Expand Down Expand Up @@ -557,6 +561,9 @@ per decoded header or trailer section is 256.

`cancel-http2-stream` emits `RST_STREAM` for an opened client stream.
`ping-http2-connection` sends a PING and waits for the matching acknowledgement.
If it receives a malformed PING while waiting, it signals
`HTTP-PROTOCOL-ERROR` with operation `:http2-client` and the received frame as
detail.
`send-http2-priority-update` emits an RFC 9218 priority update.
`send-http2-goaway` and its convenience alias
`graceful-shutdown-http2-connection` make the connection drain without
Expand Down Expand Up @@ -612,9 +619,16 @@ also clamps peer HPACK table settings to 4096 bytes by default through
`&key open-connection max-connections idle-timeout max-connection-age
clock-function on-evict on-transient-close`. `max-connections` defaults to `8`
and limits retained cache entries; request-scoped connections do not count
toward that limit. On SBCL, the manager synchronizes its entry state for
concurrent callers and leases each retained connection to at most one manager
call at a time.
toward that limit, so the total number of open connections can exceed it. On
SBCL, the manager synchronizes its entry state for concurrent callers. Ordinary
single-request calls with the same key can share one retained connection; its
scheduler assigns distinct stream IDs and observes the peer's concurrent-stream
and flow-control limits. A batch call holds an exclusive lease on its selected
connection. A concurrent call that cannot share an active entry uses a
request-scoped connection.
There is no separate cap on total open connections. Enforcing one would require
the manager to define how concurrent calls wait or fail when all sockets are in
use, so `max-connections` remains a retained-cache limit.
By default, the cache key includes the request scheme, host, port, proxy plan,
and proxy.
Supplying `connection-key` to a send function replaces that default; keys are
Expand All @@ -626,11 +640,10 @@ Each invocation passes `:timeout` and `:deadline`; proxy-routed requests also
pass `:proxy-plan` and `:proxy`. Caller-supplied `open-connection`,
`clock-function`, `on-evict`, and `on-transient-close` functions may run
concurrently with manager calls, so they must synchronize any shared state
they use. A request for a key whose retained connection is busy uses a
request-scoped connection. For a different key, the manager can create a
retained entry while below the cap, or evict the least-recently-used idle
entry at capacity. If the cache is full and every retained entry is active,
it uses a request-scoped connection.
they use. For a different key, the manager can create a retained entry while
below the cap, or evict the least-recently-used idle entry at capacity. If the
cache is full and every retained entry is active, a request for another key
uses a request-scoped connection.

`idle-timeout`, `max-connection-age`, and `on-evict` default to `nil`. Idle
expiry is based on the time since the last completed manager call; maximum age
Expand Down Expand Up @@ -667,7 +680,17 @@ session multiplexes the streams.
`make-http2-connection-manager-transport` adapts a manager to the high-level
client transport protocol. The two send functions and the function returned by
`make-http2-connection-manager-transport` each accept and forward the per-section
`max-fields` limit.
`max-fields` limit. Per-request `max-header-bytes`, `max-fields`, and
`max-body-bytes` values can tighten the selected connection's limits; they
cannot raise them.

While dispatching received response frames, malformed headers and body-limit
violations fail the connection and every active request on it. RFC 9113
§5.4.1 permits treating a stream error as a connection error. A per-request
timeout, callback error, explicit stream cancellation, or peer `RST_STREAM`
uses the stream-level handling path. If writing a required `RST_STREAM` fails,
the connection also fails.

`clear-http2-connection-manager` is idempotent. It retires
all retained connections without closing the manager and returns it on success.
A call whose connection was still opening when clear began may finish using
Expand Down
Loading
Loading