vxx2 / vlib / net / http / h2_mux_conn.v
1701 lines · 1649 sloc · 62.53 KB · 89607c731290ee2c148c11541f73c40b23b00481
Raw
1// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved.
2// Use of this source code is governed by an MIT license
3// that can be found in the LICENSE file.
4module http
5
6import sync
7import time
8
9// This file implements a multiplexed HTTP/2 client connection: one connection
10// carries many concurrent request streams. A background reader thread (the
11// only reader of the transport, and the sole owner of the HPACK decoder and
12// read buffer) demuxes incoming frames to per-stream state; request threads
13// wait on their stream's condition variable. It complements the synchronous
14// single-stream H2Conn in h2_conn.v, which remains for the one-shot paths.
15//
16// Locking protocol — a thread holds at most one of {smu, fmu, wmu} at a time,
17// with two permitted nestings: wmu → smu and smu → fmu (so wmu → smu → fmu).
18// No lock is ever taken in the reverse direction, which keeps the order
19// acyclic. A stream's own mu is never held while taking a connection lock.
20// The smu → fmu nesting is what serializes stream registration (which assigns
21// the stream's initial send window) against WINDOW_UPDATE credit and SETTINGS
22// initial-window deltas, so early credit can never be lost.
23// wmu transport writes, the HPACK encoder, stream-id allocation +
24// registration + HEADERS send (one atomic critical section), and
25// the lazy connection preface.
26// fmu/fcv flow control: the connection send window, every stream's send
27// window, and the peer's initial-window/max-frame mirrors. fcv is
28// broadcast whenever a window can grow and when the connection
29// dies, so blocked senders always wake.
30// smu connection state: the stream map, refcount, active stream count,
31// goaway/closed/shutting_down, and the peer max-streams mirror.
32
33// h2_max_recv_header_block caps the total size of a received HEADERS(+
34// CONTINUATION) block. Without it a peer can stream unbounded CONTINUATION
35// frames and exhaust memory (the CONTINUATION-flood DoS, CVE-2024-27316 class).
36const h2_max_recv_header_block = 1024 * 1024
37
38// h2_max_pending_preface_acks caps how many control-frame ACKs (SETTINGS + PING)
39// may be deferred while waiting to send the lazy client preface. A well-behaved
40// server sends only its preface SETTINGS (and perhaps a PING) before ours, so a
41// small bound is ample; without it a peer that holds the connection pre-handshake
42// could flood PING/SETTINGS and grow the deferred-ACK buffers without limit.
43const h2_max_pending_preface_acks = 64
44
45// h2_err_retryable_code tags stream errors where the request provably never
46// reached server processing (GOAWAY-unprocessed, connection closed before the
47// request was sent, admission refused), so it is safe to retry on a fresh
48// connection even for non-idempotent methods.
49pub const h2_err_retryable_code = -20012
50
51// h2_retryable_error builds an error carrying h2_err_retryable_code.
52fn h2_retryable_error(reason string) IError {
53 return error_with_code('h2: ${reason}', h2_err_retryable_code)
54}
55
56// all_digits reports whether s is non-empty and every byte is an ASCII digit.
57// Header values must be validated with this before lenient string.int()/.u64()
58// parsing, which otherwise accept malformed input like '200 OK' or '12junk'.
59fn all_digits(s string) bool {
60 if s.len == 0 {
61 return false
62 }
63 for ch in s {
64 if ch < `0` || ch > `9` {
65 return false
66 }
67 }
68 return true
69}
70
71// h2_conn_specific_headers are connection-specific header fields that MUST NOT
72// appear in any HTTP/2 message (RFC 9113 §8.2.2). A received response or trailer
73// carrying one is malformed. (TE is the request-only exception and is handled on
74// the send side, so it is not listed here.)
75const h2_conn_specific_headers = ['connection', 'keep-alive', 'proxy-connection', 'transfer-encoding',
76 'upgrade']
77
78// h2_response_field_error returns a non-empty reason when a regular (non-pseudo)
79// received header field is malformed per RFC 9113 §8.2, or '' when it is valid.
80// Field names must be non-empty and lowercase (§8.2.1), and connection-specific
81// fields are forbidden (§8.2.2). A malformed field makes the whole message
82// malformed (§8.1.1); the mux path resets the stream and the sync path fails the
83// request rather than delivering it. Pseudo-header validity is checked at the
84// call site (the set of valid pseudo-headers differs between headers/trailers).
85fn h2_response_field_error(name string) string {
86 if name.len == 0 {
87 return 'empty header field name'
88 }
89 for ch in name {
90 if ch >= `A` && ch <= `Z` {
91 return 'uppercase header field name "${name}"'
92 }
93 }
94 if name in h2_conn_specific_headers {
95 return 'connection-specific header field "${name}"'
96 }
97 return ''
98}
99
100// h2_header_list_size returns the RFC 9113 §6.5.2 size of a header list: the sum
101// over all fields of (name length + value length + 32 octets of per-field
102// overhead). Used to honor the peer's advisory SETTINGS_MAX_HEADER_LIST_SIZE.
103fn h2_header_list_size(fields []H2HeaderField) u64 {
104 mut total := u64(0)
105 for f in fields {
106 total += u64(f.name.len) + u64(f.value.len) + 32
107 }
108 return total
109}
110
111// H2MuxStream is the client-side state of one in-flight request stream.
112@[heap]
113struct H2MuxStream {
114mut:
115 id u32
116 // --- response state, guarded by mu, signaled via cv ---
117 mu &sync.Mutex = unsafe { nil }
118 cv &sync.Cond = unsafe { nil }
119 status int
120 resp_headers []H2HeaderField
121 resp_trailers []H2HeaderField // non-pseudo fields from a trailing HEADERS block
122 headers_done bool
123 chunks [][]u8 // DATA payloads appended by the reader, drained by the requester
124 body_rcvd u64 // cumulative DATA bytes received
125 ended bool // END_STREAM, RST, or connection death
126 err string // non-empty: the stream failed
127 retryable bool // the failure is safe to retry on a fresh connection
128 sent_headers bool // the request HEADERS reached the transport
129 cancelled bool // requester abandoned the stream; drop+credit late DATA
130 send_closed bool // we closed our send side (sent END_STREAM or RST_STREAM)
131 // --- send state, guarded by the connection's fmu ---
132 send_window i64
133 send_dead bool // RST/GOAWAY: wake a body sender blocked on flow-control credit
134 // --- receive state, guarded by mu ---
135 recv_window i64 = i64(h2_default_initial_window) // our receive window (what we advertised)
136}
137
138fn new_h2_mux_stream() &H2MuxStream {
139 mu := sync.new_mutex()
140 return &H2MuxStream{
141 mu: mu
142 cv: sync.new_cond(mu)
143 }
144}
145
146// fail marks the stream failed and wakes its requester.
147fn (mut s H2MuxStream) fail(msg string, retryable bool) {
148 s.mu.lock()
149 if !s.ended {
150 s.err = msg
151 s.retryable = retryable
152 s.ended = true
153 s.cv.signal()
154 }
155 s.mu.unlock()
156}
157
158// H2MuxConn is a multiplexed client-side HTTP/2 connection, safe for
159// concurrent requests from multiple threads.
160@[heap]
161pub struct H2MuxConn {
162mut:
163 transport H2Transport
164 // close_transport is called exactly once during teardown and MUST close or
165 // interrupt the transport so the reader thread (blocked in transport.read())
166 // can exit. It is required (non-nil): the H2Transport interface has no close()
167 // of its own, so this is the only way to unblock the reader. new_h2_mux_conn
168 // panics on a nil closer.
169 close_transport fn () = unsafe { nil }
170 // --- guarded by wmu ---
171 wmu &sync.Mutex = sync.new_mutex()
172 encoder H2HpackEncoder
173 next_stream_id u32 = 1
174 handshaked bool
175 pending_settings_acks int // count of SETTINGS frames received before our preface; each needs one ACK
176 pending_ping_acks [][]u8 // PING frames received before our preface; each needs an ACK with the same data
177 peer_max_header_list_size u32 = max_u32 // peer SETTINGS_MAX_HEADER_LIST_SIZE (advisory, §6.5.2)
178 // --- guarded by fmu, fcv signals growth/death ---
179 fmu &sync.Mutex = unsafe { nil }
180 fcv &sync.Cond = unsafe { nil }
181 send_window i64 = i64(h2_default_initial_window)
182 peer_initial_window i64 = i64(h2_default_initial_window)
183 peer_max_frame u32 = h2_default_max_frame_size
184 fmu_dead bool // mirror of `closed` for blocked senders
185 // --- guarded by smu ---
186 smu &sync.Mutex = sync.new_mutex()
187 streams map[u32]&H2MuxStream
188 refs int = 1 // the owner's (pool's) reference; +1 per in-flight request
189 active_streams int
190 max_streams u32 = 100 // our own cap on concurrent streams
191 peer_max_streams u32 = max_u32
192 goaway bool
193 goaway_last u32
194 closed bool // the reader has exited; only the reader sets this
195 shutting_down bool
196 transport_torn_down bool // close_transport has been (or is being) called
197 conn_err string
198 idle_since time.Time
199 // --- guarded by recv_wmu ---
200 // Inbound flow-control: tracks how much more data the peer is allowed to
201 // send us (our advertised receive windows). Debited on DATA arrival,
202 // replenished when we send WINDOW_UPDATE. recv_wmu is always acquired
203 // solo — never while holding any other connection lock.
204 recv_wmu &sync.Mutex = sync.new_mutex()
205 conn_recv_window i64 = i64(h2_default_initial_window)
206 // --- reader-thread private (no locks) ---
207 decoder H2HpackDecoder
208 rbuf []u8
209}
210
211// new_h2_mux_conn creates a multiplexed connection over `transport` and starts
212// its background reader. The connection preface is sent lazily with the first
213// request. `close_transport` is REQUIRED (non-nil): it is called once at final
214// teardown and MUST close/interrupt the transport so the blocked reader exits.
215// A nil closer is a programming error and panics here, because the reader can
216// only be unblocked by closing the transport (see the field comment).
217//
218// CONCURRENCY REQUIREMENT: the background reader calls `transport.read()` while
219// request threads call `transport.write()`, so `transport` MUST be safe for a
220// read and a write to run simultaneously on separate threads. A raw TCP socket
221// is (it is full-duplex); a TLS connection is NOT — one OpenSSL/mbedTLS `SSL`
222// object must not be read and written at the same time. Wiring this to a TLS
223// backend therefore requires a concurrent-safe transport adapter (a serialized
224// or non-blocking I/O loop); a single blocking I/O mutex is insufficient because
225// it would deadlock the blocked reader. That adapter is provided when the pooled
226// transport is wired up (see the HTTP connection-pooling Phase 3 work); until
227// then this type is exercised only over a full-duplex in-memory pipe in tests.
228pub fn new_h2_mux_conn(transport H2Transport, close_transport fn ()) &H2MuxConn {
229 if close_transport == unsafe { nil } {
230 // The reader thread spawned below blocks in transport.read(); the
231 // H2Transport interface has no close(), so this callback is the only way
232 // to interrupt that read at teardown. Without it an idle retirement or
233 // last-reference release would leak the reader thread and the connection.
234 panic('new_h2_mux_conn: close_transport must be non-nil so teardown can wake the reader')
235 }
236 fmu := sync.new_mutex()
237 mut c := &H2MuxConn{
238 transport: transport
239 close_transport: close_transport
240 fmu: fmu
241 fcv: sync.new_cond(fmu)
242 idle_since: time.now()
243 }
244 spawn c.read_loop()
245 return c
246}
247
248// can_take_new_request reports whether a new request may be admitted on this
249// connection right now (it may still be refused later under smu).
250pub fn (mut c H2MuxConn) can_take_new_request() bool {
251 c.smu.lock()
252 defer {
253 c.smu.unlock()
254 }
255 limit := if c.peer_max_streams < c.max_streams { c.peer_max_streams } else { c.max_streams }
256 return !c.closed && !c.goaway && !c.shutting_down && u32(c.active_streams) < limit
257}
258
259// shutdown_when_idle asks the connection to retire: no new requests are
260// admitted, and once no requests are in flight and the owner's reference is
261// released, the transport is closed.
262pub fn (mut c H2MuxConn) shutdown_when_idle() {
263 c.smu.lock()
264 c.shutting_down = true
265 c.smu.unlock()
266}
267
268// release drops the owner's reference. The connection tears its transport
269// down once the reader has exited and all references are gone.
270pub fn (mut c H2MuxConn) release() {
271 c.drop_ref()
272}
273
274fn (mut c H2MuxConn) drop_ref() {
275 c.smu.lock()
276 c.refs--
277 last_ref := c.refs <= 0
278 c.smu.unlock()
279 if last_ref {
280 // Tear the transport down even if the reader has not exited yet: closing
281 // it interrupts the reader's blocking read so it can exit. Otherwise an
282 // idle connection that is shut down and released leaks the reader thread
283 // and the socket, because the reader only sets `closed` after a read
284 // error/timeout, which never arrives on a silent transport.
285 c.teardown_transport()
286 }
287}
288
289// teardown_transport runs the close_transport callback exactly once. Calling it
290// while the reader is still blocked in transport.read() interrupts that read, so
291// the reader observes the closed transport and exits via fail_conn.
292fn (mut c H2MuxConn) teardown_transport() {
293 c.smu.lock()
294 already := c.transport_torn_down
295 c.transport_torn_down = true
296 c.smu.unlock()
297 if !already && c.close_transport != unsafe { nil } {
298 c.close_transport()
299 }
300}
301
302// --- request side -----------------------------------------------------------
303
304// do sends one request over the connection, concurrently with other streams,
305// and returns its response. Errors carrying h2_err_retryable_code are safe to
306// retry on a fresh connection.
307pub fn (mut c H2MuxConn) do(req H2ClientRequest) !H2ClientResponse {
308 // Admission.
309 c.smu.lock()
310 if c.closed {
311 reason := if c.conn_err != '' { c.conn_err } else { 'connection is closed' }
312 c.smu.unlock()
313 return h2_retryable_error(reason)
314 }
315 if c.goaway || c.shutting_down {
316 c.smu.unlock()
317 return h2_retryable_error('connection is shutting down')
318 }
319 limit := if c.peer_max_streams < c.max_streams { c.peer_max_streams } else { c.max_streams }
320 if u32(c.active_streams) >= limit {
321 c.smu.unlock()
322 return h2_retryable_error('connection is at its concurrent stream limit')
323 }
324 c.refs++
325 c.active_streams++
326 c.smu.unlock()
327
328 mut s := new_h2_mux_stream()
329 resp := c.do_on_stream(mut s, req) or {
330 c.finish_stream(mut s)
331 return err
332 }
333 c.finish_stream(mut s)
334 return resp
335}
336
337// finish_stream removes the stream from the connection and releases the
338// request's reference (possibly triggering teardown).
339fn (mut c H2MuxConn) finish_stream(mut s H2MuxStream) {
340 c.smu.lock()
341 c.streams.delete(s.id)
342 c.active_streams--
343 c.idle_since = time.now()
344 c.smu.unlock()
345 // Credit any DATA bytes that were queued in s.chunks but never drained.
346 // On the success path wait_response empties chunks before returning, so
347 // queued is 0. On the error path (do_on_stream returned early, e.g. after
348 // a RST_STREAM arrived mid-upload), chunks may hold bytes whose flow-size
349 // was already debited from conn_recv_window; without this credit those bytes
350 // are silently dropped, permanently shrinking the connection receive window.
351 // Setting cancelled prevents a DATA frame in flight from re-queuing onto
352 // the now-deregistered stream and leaking more bytes.
353 s.mu.lock()
354 s.cancelled = true
355 mut queued := u64(0)
356 for ch in s.chunks {
357 queued += u64(ch.len)
358 }
359 s.chunks.clear()
360 s.mu.unlock()
361 if queued > 0 {
362 c.send_conn_window_update(u32(queued)) or { c.note_write_failure() }
363 }
364 c.drop_ref()
365}
366
367fn (mut c H2MuxConn) do_on_stream(mut s H2MuxStream, req H2ClientRequest) !H2ClientResponse {
368 mut fields := [
369 H2HeaderField{':method', req.method},
370 H2HeaderField{':scheme', req.scheme},
371 H2HeaderField{':authority', req.authority},
372 H2HeaderField{':path', req.path},
373 ]
374 for h in req.headers {
375 fields << h
376 }
377 has_body := req.body.len > 0
378
379 // Stream-id allocation, registration, HPACK encoding and the HEADERS(+
380 // CONTINUATION) send form one wmu critical section: ids must hit the wire
381 // in increasing order, header blocks must be contiguous, and the stream
382 // must be registered before its first byte is sent so the reader can
383 // always deliver the response.
384 c.wmu.lock()
385 c.handshake_locked() or {
386 // Preface write failed: tear down the transport so the reader exits
387 // and the pool stops admitting new work to this dead connection.
388 // Without this, closed/shutting_down stay false and the pool can keep
389 // dispatching to a connection whose write side is already broken.
390 c.wmu.unlock()
391 c.note_write_failure()
392 return h2_retryable_error('connection handshake failed: ${err.msg()}')
393 }
394 // RFC 9113 §6.5.2: honor the peer's advisory SETTINGS_MAX_HEADER_LIST_SIZE.
395 // Refuse an over-limit request here rather than emit it and have the server
396 // reject it (e.g. 431). Not retryable: a fresh connection to the same peer
397 // carries the same limit. Read under wmu (apply_peer_settings sets it there).
398 peer_max_list := c.peer_max_header_list_size
399 if peer_max_list != max_u32 && h2_header_list_size(fields) > u64(peer_max_list) {
400 c.wmu.unlock()
401 return error('h2: request header list (${h2_header_list_size(fields)} bytes) exceeds peer SETTINGS_MAX_HEADER_LIST_SIZE (${peer_max_list})')
402 }
403 if c.next_stream_id > u32(0x7fff_ffff) {
404 // RFC 7540 §5.1.1: client stream IDs are odd and must not exceed 2^31-1.
405 // Retire this connection and let the caller open a fresh one.
406 c.smu.lock()
407 c.shutting_down = true
408 c.smu.unlock()
409 c.wmu.unlock()
410 return h2_retryable_error('stream ID space exhausted')
411 }
412 s.id = c.next_stream_id
413 c.next_stream_id += 2
414 // Mark the HEADERS as sent *before* the stream becomes visible to the
415 // reader: pessimistic, so a connection death racing this section can never
416 // classify a request whose HEADERS may have reached the wire as safe to
417 // replay. (The stream is still private here, so no lock is needed.)
418 s.sent_headers = true
419 c.smu.lock() // wmu -> smu -> fmu is the permitted lock nesting
420 // A terminal event (GOAWAY, or fail_conn after the reader saw the transport
421 // die) can land while we are blocked on wmu, after do()'s admission check
422 // already passed. Such an event fails every stream in the map, but this one
423 // is not registered yet, so it would slip through and wait_response could
424 // block forever. Recheck under smu — the same lock those events use to set
425 // these flags — and abort before registering or sending. The HEADERS never
426 // hit the wire here, so the request is safe to retry on a fresh connection.
427 // Also recheck the peer's stream limit: SETTINGS_MAX_CONCURRENT_STREAMS can
428 // arrive while we are blocked on wmu, lowering the limit below active_streams.
429 if c.closed || c.goaway || c.shutting_down {
430 reason := if c.conn_err != '' { c.conn_err } else { 'connection is shutting down' }
431 c.smu.unlock()
432 c.wmu.unlock()
433 return h2_retryable_error(reason)
434 }
435 recheck_limit := if c.peer_max_streams < c.max_streams {
436 c.peer_max_streams
437 } else {
438 c.max_streams
439 }
440 if u32(c.active_streams) > recheck_limit {
441 c.smu.unlock()
442 c.wmu.unlock()
443 return h2_retryable_error('peer lowered concurrent stream limit; retrying on a fresh connection')
444 }
445 // If this was the last valid client stream ID (RFC 7540 §5.1.1: max 2^31-1),
446 // retire the connection so can_take_new_request() returns false immediately.
447 // Without this, the pool dispatches one extra request that hits the admission
448 // check at the top of do_on_stream and fails with a retryable error.
449 if c.next_stream_id > u32(0x7fff_ffff) {
450 c.shutting_down = true
451 }
452 c.fmu.lock()
453 // The initial send window must be assigned atomically with registration:
454 // the WINDOW_UPDATE and SETTINGS handlers also nest smu -> fmu, so credit
455 // or a delta arriving right after our HEADERS serializes after this
456 // assignment instead of being overwritten by it.
457 s.send_window = c.peer_initial_window
458 c.streams[s.id] = s
459 c.fmu.unlock()
460 c.smu.unlock()
461 block := c.encoder.encode(fields)
462 c.send_header_block_locked(s.id, block, !has_body) or {
463 c.wmu.unlock()
464 c.note_write_failure()
465 // The HEADERS may have partially hit the wire; not safe to blind-retry
466 // unless the transport wrote nothing, which we cannot distinguish here.
467 return error('h2: failed to send request headers: ${err.msg()}')
468 }
469 c.wmu.unlock()
470
471 if has_body {
472 c.send_body_on_stream(mut s, req.body)!
473 } else {
474 // The HEADERS carried END_STREAM, so our send side is now closed.
475 s.mu.lock()
476 s.send_closed = true
477 s.mu.unlock()
478 }
479 return c.wait_response(mut s, req)
480}
481
482// handshake_locked sends the connection preface and our SETTINGS once.
483// Callers must hold wmu.
484fn (mut c H2MuxConn) handshake_locked() ! {
485 if c.handshaked {
486 return
487 }
488 mut buf := h2_client_preface.bytes()
489 buf << H2Frame(H2SettingsFrame{
490 settings: [
491 H2Setting{h2_settings_enable_push, 0},
492 H2Setting{h2_settings_initial_window_size, h2_default_initial_window},
493 H2Setting{h2_settings_max_frame_size, h2_default_max_frame_size},
494 ]
495 }).encode()
496 c.write_all_locked(buf)!
497 c.handshaked = true
498 // If the reader processed server frames before we sent our preface, ACKs were
499 // deferred to preserve client-preface-first ordering (RFC 7540 §3.5). Flush
500 // them now: one SETTINGS ACK per received non-ACK SETTINGS, then any PING ACKs.
501 for _ in 0 .. c.pending_settings_acks {
502 c.write_all_locked(H2Frame(H2SettingsFrame{ ack: true }).encode())!
503 }
504 c.pending_settings_acks = 0
505 for data in c.pending_ping_acks {
506 c.write_all_locked(H2Frame(H2PingFrame{ ack: true, data: data }).encode())!
507 }
508 c.pending_ping_acks.clear()
509}
510
511// send_header_block_locked writes a header block as HEADERS(+CONTINUATION)
512// frames. Callers must hold wmu.
513fn (mut c H2MuxConn) send_header_block_locked(stream_id u32, block []u8, end_stream bool) ! {
514 c.fmu.lock()
515 mut max := int(c.peer_max_frame)
516 c.fmu.unlock()
517 if block.len <= max {
518 // Re-read peer_max_frame right before writing to close the TOCTOU window:
519 // the peer may have lowered SETTINGS_MAX_FRAME_SIZE since the check above.
520 c.fmu.lock()
521 max = int(c.peer_max_frame)
522 c.fmu.unlock()
523 if block.len <= max {
524 c.write_all_locked(H2Frame(H2HeadersFrame{
525 stream_id: stream_id
526 fragment: block
527 end_headers: true
528 end_stream: end_stream
529 }).encode())!
530 return
531 }
532 // block no longer fits in one frame under the refreshed limit;
533 // fall through to the multi-frame path.
534 }
535 // Re-read peer_max_frame under fmu immediately before the first HEADERS write
536 // to minimise the TOCTOU window between the size-check above and this write.
537 c.fmu.lock()
538 max = int(c.peer_max_frame)
539 c.fmu.unlock()
540 // Clamp to block.len in case max grew large enough to fit the whole block.
541 first := if max < block.len { max } else { block.len }
542 c.write_all_locked(H2Frame(H2HeadersFrame{
543 stream_id: stream_id
544 fragment: block[..first]
545 end_headers: first == block.len
546 end_stream: end_stream
547 }).encode())!
548 if first == block.len {
549 return
550 }
551 mut off := first
552 for off < block.len {
553 // Re-read peer_max_frame under fmu before each CONTINUATION to minimise
554 // the TOCTOU window: the peer could send a smaller SETTINGS_MAX_FRAME_SIZE
555 // between iterations and enforce it on the next frame we send.
556 c.fmu.lock()
557 cur_max := int(c.peer_max_frame)
558 c.fmu.unlock()
559 mut next := off + cur_max
560 if next > block.len {
561 next = block.len
562 }
563 c.write_all_locked(H2Frame(H2ContinuationFrame{
564 stream_id: stream_id
565 fragment: block[off..next]
566 end_headers: next == block.len
567 }).encode())!
568 off = next
569 }
570}
571
572// send_body_on_stream writes the request body as DATA frames, bounded by both
573// the connection and stream send windows.
574fn (mut c H2MuxConn) send_body_on_stream(mut s H2MuxStream, body []u8) ! {
575 mut off := 0
576 for off < body.len {
577 // Reserve a window-bounded chunk under fmu (waiting for WINDOW_UPDATE
578 // room when both windows are exhausted), then write it under wmu.
579 c.fmu.lock()
580 for !c.fmu_dead && !s.send_dead && (c.send_window <= 0 || s.send_window <= 0) {
581 c.fcv.wait()
582 }
583 if c.fmu_dead || s.send_dead {
584 stream_dead := s.send_dead
585 c.fmu.unlock()
586 if stream_dead {
587 // send_dead is always set together with a terminal stream state,
588 // so distinguish the two by whether fail() recorded an error:
589 // - no error → the server sent END_STREAM (early final response)
590 // while we still owed body; abandon the upload and close our
591 // half (below) so wait_response can still deliver the response.
592 // - error set → RST_STREAM, GOAWAY, or connection death; surface
593 // it here, preserving retryability so a request the server never
594 // processed can be replayed on a fresh connection.
595 s.mu.lock()
596 failure := s.err
597 failure_retryable := s.retryable
598 s.mu.unlock()
599 if failure != '' {
600 if failure_retryable {
601 return h2_retryable_error(failure)
602 }
603 return error('h2: ${failure}')
604 }
605 // Early final response: the server completed and closed its half
606 // while we still owed body. RST_STREAM(CANCEL) closes our half so
607 // the server releases the stream instead of holding it half-open
608 // and counting it against its concurrency limit (RFC 9113 §8.1).
609 // The stream stays registered in c.streams, so wait_response still
610 // returns the already-received response from the s reference.
611 c.wmu.lock()
612 c.write_all_locked(H2Frame(H2RstStreamFrame{
613 stream_id: s.id
614 error_code: u32(H2ErrorCode.cancel)
615 }).encode()) or {
616 c.wmu.unlock()
617 c.note_write_failure()
618 return
619 }
620 c.wmu.unlock()
621 s.mu.lock()
622 s.send_closed = true
623 s.mu.unlock()
624 return
625 }
626 return error('h2: connection closed while sending the request body')
627 }
628 mut chunk := body.len - off
629 if chunk > int(c.peer_max_frame) {
630 chunk = int(c.peer_max_frame)
631 }
632 if i64(chunk) > c.send_window {
633 chunk = int(c.send_window)
634 }
635 if i64(chunk) > s.send_window {
636 chunk = int(s.send_window)
637 }
638 c.send_window -= i64(chunk)
639 s.send_window -= i64(chunk)
640 c.fmu.unlock()
641
642 mut next := off + chunk
643 c.wmu.lock()
644 // Re-cap chunk under wmu→fmu (permitted order): the reader may have
645 // processed SETTINGS_MAX_FRAME_SIZE and sent the ACK between our
646 // fmu.unlock() above and wmu.lock() here; re-read and return any excess.
647 c.fmu.lock()
648 if chunk > int(c.peer_max_frame) {
649 excess := i64(chunk) - i64(c.peer_max_frame)
650 c.send_window += excess
651 s.send_window += excess
652 chunk = int(c.peer_max_frame)
653 next = off + chunk
654 // Wake any goroutines sleeping in fcv.wait() because c.send_window
655 // was drained; no WINDOW_UPDATE is coming for these bytes since they
656 // were never sent.
657 c.fcv.broadcast()
658 }
659 // Also revalidate stream send window: an INITIAL_WINDOW_SIZE reduction
660 // applies a negative delta to s.send_window under fmu, so a negative
661 // value here means we over-claimed against the peer's new window. Return
662 // the excess bytes to both windows so the loop can re-wait for capacity.
663 if s.send_window < 0 {
664 trim := if i64(chunk) < -s.send_window { i64(chunk) } else { -s.send_window }
665 chunk -= int(trim)
666 s.send_window += trim
667 c.send_window += trim
668 next = off + chunk
669 c.fcv.broadcast()
670 }
671 c.fmu.unlock()
672 if chunk == 0 {
673 c.wmu.unlock()
674 continue
675 }
676 c.write_all_locked(H2Frame(H2DataFrame{
677 stream_id: s.id
678 data: body[off..next]
679 end_stream: next == body.len
680 }).encode()) or {
681 c.wmu.unlock()
682 c.note_write_failure()
683 return error('h2: failed to send request body: ${err.msg()}')
684 }
685 c.wmu.unlock()
686 off = next
687 }
688 // The final DATA frame carried END_STREAM, so our send side is now closed.
689 s.mu.lock()
690 s.send_closed = true
691 s.mu.unlock()
692}
693
694// wait_response drains the stream until it ends, honoring the request's
695// streaming callback and stop limits. Callbacks run on the requester's thread.
696fn (mut c H2MuxConn) wait_response(mut s H2MuxStream, req H2ClientRequest) !H2ClientResponse {
697 mut resp := H2ClientResponse{}
698 mut body_expected := u64(0)
699 mut has_content_length := false
700 mut got_headers := false
701 mut body_so_far := u64(0)
702 mut cancelled := false
703 s.mu.lock()
704 for {
705 // Drain everything currently buffered.
706 if !got_headers && s.headers_done {
707 resp.status = s.status
708 for f in s.resp_headers {
709 resp.headers << f
710 // on_response_headers already rejects a non-numeric Content-Length,
711 // but guard the lenient u64() at the parse site too so this stays
712 // correct even if that upstream check is ever changed.
713 if f.name == 'content-length' && all_digits(f.value) {
714 body_expected = f.value.u64()
715 has_content_length = true
716 }
717 }
718 got_headers = true
719 }
720 mut drained := u64(0)
721 for s.chunks.len > 0 {
722 chunk := s.chunks[0]
723 s.chunks.delete(0)
724 body_so_far += u64(chunk.len)
725 drained += u64(chunk.len)
726 if req.stop_copying_limit < 0
727 || i64(body_so_far) - i64(chunk.len) < req.stop_copying_limit {
728 if req.stop_copying_limit >= 0 && i64(body_so_far) > req.stop_copying_limit {
729 remaining := req.stop_copying_limit - (i64(body_so_far) - i64(chunk.len))
730 if remaining > 0 {
731 resp.body << chunk[..int(remaining)]
732 }
733 } else {
734 resp.body << chunk
735 }
736 }
737 if req.on_data != unsafe { nil } {
738 // Run the user callback outside the stream lock so it can
739 // block without stalling the reader's delivery.
740 s.mu.unlock()
741 req.on_data(chunk, body_so_far, body_expected, resp.status) or {
742 // The callback aborted the request: credit the connection
743 // window for what we consumed this round and RST the stream,
744 // just like the stop_receiving_limit path, so the connection
745 // window does not leak and the peer stops sending.
746 if drained > 0 {
747 c.send_conn_window_update(u32(drained)) or {}
748 }
749 c.cancel_stream(mut s)
750 return err
751 }
752 s.mu.lock()
753 }
754 if req.stop_receiving_limit >= 0 && i64(body_so_far) >= req.stop_receiving_limit {
755 cancelled = true
756 break
757 }
758 }
759 ended := s.ended
760 serr := s.err
761 retryable := s.retryable
762 if cancelled {
763 s.mu.unlock()
764 // The connection-level window must still be credited for the bytes
765 // this round consumed, or the connection's receive window shrinks
766 // permanently for every other stream.
767 if drained > 0 {
768 c.send_conn_window_update(u32(drained)) or {}
769 }
770 c.cancel_stream(mut s)
771 if !got_headers {
772 return error('h2: stream cancelled before a response arrived')
773 }
774 return resp
775 }
776 if drained > 0 {
777 // Replenish flow control for what was just consumed, outside s.mu.
778 s.mu.unlock()
779 c.send_window_updates(s.id, u32(drained)) or {}
780 s.mu.lock()
781 // New chunks may have arrived while unlocked; loop and re-drain.
782 if s.chunks.len > 0 || (s.ended && !ended) {
783 continue
784 }
785 }
786 if ended {
787 // Surface any trailers (a second HEADERS block) into the response
788 // headers, mirroring the synchronous H2Conn.read_response path. The
789 // stream has ended, so resp_trailers is complete; copy under the lock.
790 for f in s.resp_trailers {
791 resp.headers << f
792 }
793 s.mu.unlock()
794 if serr != '' {
795 if retryable {
796 return h2_retryable_error(serr)
797 }
798 return error('h2: ${serr}')
799 }
800 if !got_headers {
801 return error('h2: stream closed without a response')
802 }
803 // RFC 9110 §8.6: a Content-Length must match the bytes received.
804 // Skip for responses defined to carry no body — HEAD requests and
805 // 204/304 status codes — where Content-Length describes the absent
806 // representation rather than transmitted DATA.
807 body_allowed := req.method != 'HEAD' && resp.status != 204 && resp.status != 304
808 if has_content_length && body_allowed && body_so_far != body_expected {
809 return error('h2: response body length ${body_so_far} does not match Content-Length ${body_expected}')
810 }
811 return resp
812 }
813 s.cv.wait()
814 }
815 return resp
816}
817
818// cancel_stream aborts a stream early (stop_receiving_limit): the stream is
819// deregistered first and then RST_STREAM is sent, so any in-flight late DATA
820// for it is handled by the reader's unknown-stream backstop (connection-level
821// WINDOW_UPDATE), keeping the connection fully usable for other streams.
822fn (mut c H2MuxConn) cancel_stream(mut s H2MuxStream) {
823 // Deregister first, then send RST_STREAM: once the stream is gone from
824 // c.streams, any DATA the reader already had in flight for it hits the
825 // unknown-stream backstop, which credits the connection-level receive
826 // window. If we sent RST first, that in-flight DATA could still find the
827 // registered stream and be queued without a WINDOW_UPDATE (the cancelled
828 // requester has stopped draining), permanently leaking connection window.
829 c.smu.lock()
830 c.streams.delete(s.id)
831 c.smu.unlock()
832 // DATA already queued before cancellation consumed the connection receive
833 // window but will never be drained; credit it back (its padding was already
834 // credited on receipt, so only the chunk data remains). Setting `cancelled`
835 // under s.mu also makes a frame still in flight credit-and-drop in
836 // on_response_data instead of queuing onto a stream nobody drains. Without
837 // this, repeated early cancellations permanently shrink the connection window.
838 s.mu.lock()
839 s.cancelled = true
840 mut queued := u64(0)
841 for ch in s.chunks {
842 queued += u64(ch.len)
843 }
844 s.chunks.clear()
845 s.mu.unlock()
846 if queued > 0 {
847 // A write failure here means the transport is already dead; tear it down
848 // immediately so the pool stops reusing it. note_write_failure is
849 // sufficient — the RST_STREAM is skipped since the peer will not receive
850 // it on a dead transport.
851 c.send_conn_window_update(u32(queued)) or {
852 c.note_write_failure()
853 return
854 }
855 }
856 c.wmu.lock()
857 c.write_all_locked(H2Frame(H2RstStreamFrame{
858 stream_id: s.id
859 error_code: u32(H2ErrorCode.cancel)
860 }).encode()) or {
861 c.wmu.unlock()
862 c.note_write_failure()
863 return
864 }
865 c.wmu.unlock()
866}
867
868// send_conn_window_update replenishes only the connection-level receive
869// window (used when the stream itself is being cancelled).
870fn (mut c H2MuxConn) send_conn_window_update(n u32) ! {
871 if n == 0 {
872 return
873 }
874 // Credit our tracked window before sending the frame so the budget is
875 // never in deficit for longer than necessary.
876 c.recv_wmu.lock()
877 c.conn_recv_window += i64(n)
878 c.recv_wmu.unlock()
879 buf := H2Frame(H2WindowUpdateFrame{
880 stream_id: 0
881 window_size_increment: n
882 }).encode()
883 c.wmu.lock()
884 c.write_all_locked(buf) or {
885 c.wmu.unlock()
886 c.note_write_failure()
887 return err
888 }
889 c.wmu.unlock()
890}
891
892// send_window_updates replenishes both the connection and stream receive
893// windows after the requester consumed `n` body bytes.
894fn (mut c H2MuxConn) send_window_updates(stream_id u32, n u32) ! {
895 if n == 0 {
896 return
897 }
898 // Credit both tracked receive windows before sending the frames.
899 c.recv_wmu.lock()
900 c.conn_recv_window += i64(n)
901 c.recv_wmu.unlock()
902 mut stream_alive := false
903 c.smu.lock()
904 if mut s := c.streams[stream_id] {
905 s.mu.lock()
906 s.recv_window += i64(n)
907 s.mu.unlock()
908 stream_alive = true
909 }
910 c.smu.unlock()
911 mut buf := H2Frame(H2WindowUpdateFrame{
912 stream_id: 0
913 window_size_increment: n
914 }).encode()
915 if stream_alive {
916 buf << H2Frame(H2WindowUpdateFrame{
917 stream_id: stream_id
918 window_size_increment: n
919 }).encode()
920 }
921 c.wmu.lock()
922 c.write_all_locked(buf) or {
923 c.wmu.unlock()
924 c.note_write_failure()
925 return err
926 }
927 c.wmu.unlock()
928}
929
930// write_all_locked writes all of `data` to the transport. Callers hold wmu.
931fn (mut c H2MuxConn) write_all_locked(data []u8) ! {
932 mut sent := 0
933 for sent < data.len {
934 n := c.transport.write(data[sent..])!
935 if n <= 0 {
936 return error('transport write returned ${n}')
937 }
938 sent += n
939 }
940}
941
942// note_write_failure handles a transport write failure: it stops admitting new
943// requests and tears the transport down. Closing it interrupts the reader's
944// blocking read so it runs fail_conn and fails every other in-flight stream,
945// instead of leaving them hung when the transport's read side does not also
946// break (a write-only failure). teardown_transport is once-guarded, so this
947// never double-closes against the reader's own teardown.
948fn (mut c H2MuxConn) note_write_failure() {
949 c.smu.lock()
950 c.shutting_down = true
951 c.smu.unlock()
952 // Interrupt the reader's blocking read if a closer can, then fail the
953 // connection directly. fail_conn is idempotent (guarded by c.closed), so a
954 // later reader invocation is a safe no-op. Calling it unconditionally — not
955 // only when close_transport is nil — wakes every in-flight stream
956 // immediately, closing both the window before the reader notices the dead
957 // transport and the case of a closer that cannot interrupt a blocking read.
958 c.teardown_transport()
959 c.fail_conn('transport write failure')
960}
961
962// --- reader side -------------------------------------------------------------
963
964// read_loop runs on the connection's background thread: it is the only reader
965// of the transport and demuxes every incoming frame.
966fn (mut c H2MuxConn) read_loop() {
967 for {
968 frame := c.mux_read_frame() or {
969 if is_transport_timeout_error(err) {
970 if c.reader_should_exit() {
971 c.fail_conn('connection retired while idle')
972 return
973 }
974 continue
975 }
976 c.fail_conn('connection lost: ${err.msg()}')
977 return
978 }
979 c.dispatch_frame(frame) or {
980 c.fail_conn(err.msg())
981 return
982 }
983 }
984}
985
986// reader_should_exit lets an idle reader retire the connection on shutdown.
987fn (mut c H2MuxConn) reader_should_exit() bool {
988 c.smu.lock()
989 defer {
990 c.smu.unlock()
991 }
992 return c.shutting_down && c.active_streams == 0
993}
994
995// is_transport_timeout_error recognizes read-timeout errors, which wake the
996// reader for shutdown checks rather than killing the connection.
997fn is_transport_timeout_error(err IError) bool {
998 msg := err.msg().to_lower()
999 return msg.contains('timed out') || msg.contains('timeout')
1000}
1001
1002// mux_read_frame reads and decodes one frame from the transport, enforcing
1003// the receive limit we advertised to the peer in our own SETTINGS. This is
1004// h2_default_max_frame_size, which H2MuxConn always sends and never renegotiates.
1005// (c.peer_max_frame is the peer's receive limit — our outbound cap — and must
1006// not be used here.)
1007fn (mut c H2MuxConn) mux_read_frame() !H2Frame {
1008 c.mux_fill_at_least(h2_frame_header_len)!
1009 header := h2_parse_frame_header(c.rbuf)!
1010 if header.length > h2_default_max_frame_size {
1011 return error('frame larger than SETTINGS_MAX_FRAME_SIZE (${header.length})')
1012 }
1013 total := h2_frame_header_len + int(header.length)
1014 c.mux_fill_at_least(total)!
1015 frame := h2_parse_frame(header, c.rbuf[h2_frame_header_len..total])!
1016 c.rbuf = c.rbuf[total..].clone()
1017 return frame
1018}
1019
1020// mux_fill_at_least reads from the transport until rbuf holds n bytes.
1021fn (mut c H2MuxConn) mux_fill_at_least(n int) ! {
1022 for c.rbuf.len < n {
1023 mut tmp := []u8{len: h2_conn_read_chunk}
1024 got := c.transport.read(mut tmp)!
1025 if got <= 0 {
1026 return error('connection closed by peer')
1027 }
1028 c.rbuf << tmp[..got]
1029 }
1030}
1031
1032fn (mut c H2MuxConn) dispatch_frame(frame H2Frame) ! {
1033 match frame {
1034 H2SettingsFrame {
1035 if !frame.ack {
1036 c.apply_peer_settings(frame.settings)!
1037 c.wmu.lock()
1038 if !c.handshaked {
1039 // RFC 7540 §3.5: no frame may precede the client connection
1040 // preface. Defer the ACK; handshake_locked() will flush it
1041 // immediately after sending the preface. Use a counter so
1042 // multiple SETTINGS frames each get their own ACK.
1043 c.pending_settings_acks++
1044 over := c.pending_settings_acks + c.pending_ping_acks.len > h2_max_pending_preface_acks
1045 c.wmu.unlock()
1046 if over {
1047 return error('h2: too many control frames before the client preface')
1048 }
1049 } else {
1050 c.write_all_locked(H2Frame(H2SettingsFrame{
1051 ack: true
1052 }).encode()) or {
1053 c.wmu.unlock()
1054 return error('failed to ack SETTINGS: ${err.msg()}')
1055 }
1056 c.wmu.unlock()
1057 }
1058 }
1059 }
1060 H2PingFrame {
1061 if !frame.ack {
1062 c.wmu.lock()
1063 if !c.handshaked {
1064 // RFC 7540 §3.5: client preface must be the first bytes sent.
1065 // Defer the ACK; handshake_locked() will flush it after the preface.
1066 c.pending_ping_acks << frame.data
1067 over := c.pending_settings_acks + c.pending_ping_acks.len > h2_max_pending_preface_acks
1068 c.wmu.unlock()
1069 if over {
1070 return error('h2: too many control frames before the client preface')
1071 }
1072 } else {
1073 c.write_all_locked(H2Frame(H2PingFrame{
1074 ack: true
1075 data: frame.data
1076 }).encode()) or {
1077 c.wmu.unlock()
1078 return error('failed to ack PING: ${err.msg()}')
1079 }
1080 c.wmu.unlock()
1081 }
1082 }
1083 }
1084 H2WindowUpdateFrame {
1085 inc := frame.window_size_increment
1086 if frame.stream_id == 0 {
1087 // RFC 7540 6.9: a 0 increment is a connection PROTOCOL_ERROR;
1088 // a window past 2^31-1 is a connection FLOW_CONTROL_ERROR.
1089 if inc == 0 {
1090 return error('h2: connection WINDOW_UPDATE with a zero increment')
1091 }
1092 c.fmu.lock()
1093 new_window := c.send_window + i64(inc)
1094 overflow := new_window > i64(0x7fff_ffff)
1095 if !overflow {
1096 c.send_window = new_window
1097 c.fcv.broadcast()
1098 }
1099 c.fmu.unlock()
1100 if overflow {
1101 return error('h2: connection flow-control window exceeded 2^31-1')
1102 }
1103 } else {
1104 // Stream-level versions of the same rules are stream errors:
1105 // RST_STREAM the offending stream instead of killing the conn.
1106 if inc == 0 {
1107 c.reset_stream(frame.stream_id, .protocol_error,
1108 'WINDOW_UPDATE with a zero increment')
1109 return
1110 }
1111 // Hold smu across the lookup and the credit (smu -> fmu), so
1112 // this serializes with stream registration and the credit can
1113 // never be overwritten by the initial-window assignment.
1114 c.smu.lock()
1115 mut sref := c.streams[frame.stream_id] or { &H2MuxStream(unsafe { nil }) }
1116 mut overflow := false
1117 if sref != unsafe { nil } {
1118 c.fmu.lock()
1119 new_window := sref.send_window + i64(inc)
1120 overflow = new_window > i64(0x7fff_ffff)
1121 if !overflow {
1122 sref.send_window = new_window
1123 c.fcv.broadcast()
1124 }
1125 c.fmu.unlock()
1126 }
1127 c.smu.unlock()
1128 if overflow {
1129 c.reset_stream(frame.stream_id, .flow_control_error,
1130 'stream flow-control window exceeded 2^31-1')
1131 }
1132 }
1133 }
1134 H2GoawayFrame {
1135 // Take wmu before smu (the permitted wmu -> smu nesting) so setting
1136 // c.goaway serializes with do_on_stream's terminal-flag recheck, which
1137 // runs under smu while holding wmu. Without this, a GOAWAY landing
1138 // between that recheck (smu released at the end of the registration
1139 // section) and the HEADERS write (still under wmu) lets the client
1140 // open one more stream after observing GOAWAY (RFC 7540 6.8). Holding
1141 // wmu here means c.goaway cannot be set while any writer is mid-section,
1142 // so the recheck is authoritative.
1143 c.wmu.lock()
1144 c.smu.lock()
1145 c.goaway = true
1146 c.goaway_last = frame.last_stream_id
1147 mut above := []&H2MuxStream{}
1148 for id, st in c.streams {
1149 if id > frame.last_stream_id {
1150 above << st
1151 }
1152 }
1153 c.smu.unlock()
1154 c.wmu.unlock()
1155 for mut st in above {
1156 // Streams above last_stream_id were not processed by the
1157 // server, so they are safe to retry elsewhere (RFC 7540 6.8).
1158 st.fail('request not processed (GOAWAY)', true)
1159 c.wake_send(mut st)
1160 }
1161 if frame.error_code != u32(H2ErrorCode.no_error) {
1162 return error('connection error (GOAWAY ${h2_error_code_name(frame.error_code)})')
1163 }
1164 }
1165 H2HeadersFrame {
1166 c.on_response_headers(frame)!
1167 }
1168 H2DataFrame {
1169 c.on_response_data(frame)!
1170 }
1171 H2RstStreamFrame {
1172 mut s := c.lookup_stream(frame.stream_id)
1173 if s != unsafe { nil } {
1174 // REFUSED_STREAM means the server did not process the request
1175 // (RFC 7540 8.1.4), so it is safe to replay on a fresh connection
1176 // even for a non-idempotent method; any other reset code is not.
1177 retryable := frame.error_code == u32(H2ErrorCode.refused_stream)
1178 s.fail('stream reset by peer (${h2_error_code_name(frame.error_code)})', retryable)
1179 c.wake_send(mut s)
1180 }
1181 }
1182 H2ContinuationFrame {
1183 // CONTINUATION outside a header block is a connection error; the
1184 // in-block ones are consumed by on_response_headers.
1185 return error('unexpected CONTINUATION frame')
1186 }
1187 H2PushPromiseFrame {
1188 // We advertise SETTINGS_ENABLE_PUSH=0, so any PUSH_PROMISE is a
1189 // connection error (RFC 7540 6.6 / 8.2). Failing the connection
1190 // here also avoids the dropped fragment desyncing our HPACK decoder.
1191 return error('h2: unexpected PUSH_PROMISE (server push is disabled)')
1192 }
1193 else {
1194 // PRIORITY / unknown frame types: ignored per RFC 7540.
1195 }
1196 }
1197}
1198
1199// apply_peer_settings folds the peer's SETTINGS into the connection,
1200// including the retroactive initial-window delta for every open stream
1201// (RFC 7540 6.9.2). Out-of-range values that would corrupt our framing or
1202// flow-control math are rejected as a connection error (the caller turns the
1203// error into fail_conn), per RFC 7540 6.5.2 / 6.5.3.
1204fn (mut c H2MuxConn) apply_peer_settings(settings []H2Setting) ! {
1205 for st in settings {
1206 match st.id {
1207 h2_settings_header_table_size {
1208 // RFC 7541 §6.3: even if our encoder uses only literals, we MUST
1209 // emit a Dynamic Table Size Update prefix at the start of the next
1210 // HEADERS block when the peer lowers this limit. encode() emits
1211 // the update when pending_max_table_size >= 0.
1212 c.wmu.lock()
1213 c.encoder.dyn_table.set_max_size(int(st.value))
1214 c.encoder.pending_max_table_size = int(st.value)
1215 c.wmu.unlock()
1216 }
1217 h2_settings_enable_push {
1218 if st.value > 1 {
1219 // RFC 7540 6.5.2: ENABLE_PUSH must be 0 or 1.
1220 return error('h2: peer SETTINGS_ENABLE_PUSH ${st.value} is not 0 or 1')
1221 }
1222 // We never use server push, so the value is otherwise irrelevant.
1223 }
1224 h2_settings_max_concurrent_streams {
1225 c.smu.lock()
1226 c.peer_max_streams = st.value
1227 c.smu.unlock()
1228 }
1229 h2_settings_max_header_list_size {
1230 // RFC 9113 §6.5.2: advisory cap on the size of the header list we
1231 // send. Store it (under wmu, with the other send-side header state)
1232 // so do_on_stream can refuse an over-limit request locally instead
1233 // of having the server reject it after the round trip.
1234 c.wmu.lock()
1235 c.peer_max_header_list_size = st.value
1236 c.wmu.unlock()
1237 }
1238 h2_settings_initial_window_size {
1239 if st.value > u32(0x7fff_ffff) {
1240 // RFC 7540 6.5.3: values above 2^31-1 are a FLOW_CONTROL_ERROR;
1241 // they also overflow our i64 send-window arithmetic.
1242 return error('h2: peer SETTINGS_INITIAL_WINDOW_SIZE ${st.value} exceeds 2^31-1')
1243 }
1244 // smu is held across the fmu section so the snapshot of open
1245 // streams and the delta application are atomic with respect to
1246 // stream registration (which nests the same way) — a stream can
1247 // neither miss the delta nor receive it twice.
1248 c.smu.lock()
1249 c.fmu.lock()
1250 delta := i64(st.value) - c.peer_initial_window
1251 c.peer_initial_window = i64(st.value)
1252 // RFC 7540 §6.9.2: validate ALL stream windows before applying any
1253 // delta so that an overflow on stream N does not leave streams 1..N-1
1254 // in a partially updated state. Pre-validation also ensures the
1255 // broadcast below is always reached for a positive delta.
1256 for _, s in c.streams {
1257 if s.send_window + delta > i64(0x7fff_ffff) {
1258 c.fmu.unlock()
1259 c.smu.unlock()
1260 return error('h2: SETTINGS_INITIAL_WINDOW_SIZE delta overflows stream ${s.id} send window (RFC 7540 §6.9.2 FLOW_CONTROL_ERROR)')
1261 }
1262 }
1263 for _, mut s in c.streams {
1264 s.send_window += delta
1265 }
1266 if delta > 0 {
1267 c.fcv.broadcast()
1268 }
1269 c.fmu.unlock()
1270 c.smu.unlock()
1271 }
1272 h2_settings_max_frame_size {
1273 if st.value < h2_default_max_frame_size || st.value > u32(0x00ff_ffff) {
1274 // RFC 7540 6.5.2: valid range is 2^14..2^24-1. A value such as
1275 // 0 would make our HEADERS/DATA chunk step 0 and hang the send
1276 // path in a zero-length-frame loop, so fail the connection.
1277 return error('h2: peer SETTINGS_MAX_FRAME_SIZE ${st.value} out of range [16384, 16777215]')
1278 }
1279 // Take wmu (then fmu, the permitted wmu -> fmu nesting) before
1280 // lowering the cap. The send paths re-read peer_max_frame under fmu
1281 // and then write the frame under wmu, holding wmu across both; an
1282 // fmu-only update could land in the fmu-release -> write gap and let
1283 // a sender emit a frame larger than the peer's new limit
1284 // (FRAME_SIZE_ERROR). Serializing on wmu makes the senders' re-read
1285 // authoritative, mirroring the header_table_size arm above.
1286 // apply_peer_settings holds no connection lock at entry, so this
1287 // wmu acquisition cannot invert any held lock.
1288 c.wmu.lock()
1289 c.fmu.lock()
1290 c.peer_max_frame = st.value
1291 c.fmu.unlock()
1292 c.wmu.unlock()
1293 }
1294 else {} // unknown settings are ignored (RFC 7540 6.5.2)
1295 }
1296 }
1297}
1298
1299// on_response_headers assembles a complete header block (reading any
1300// CONTINUATION frames inline — the reader owns the read path), decodes it,
1301// and delivers it to the stream. Blocks for unknown streams are still decoded
1302// to keep the connection's HPACK dynamic table in sync, then dropped.
1303fn (mut c H2MuxConn) on_response_headers(frame H2HeadersFrame) ! {
1304 mut fragment := frame.fragment.clone()
1305 if !frame.end_headers {
1306 for {
1307 cont := c.mux_read_frame() or {
1308 return error('connection lost inside a header block: ${err.msg()}')
1309 }
1310 if cont is H2ContinuationFrame {
1311 if cont.stream_id != frame.stream_id {
1312 return error('CONTINUATION on the wrong stream')
1313 }
1314 fragment << cont.fragment
1315 if fragment.len > h2_max_recv_header_block {
1316 return error('h2: response header block exceeds ${h2_max_recv_header_block} bytes')
1317 }
1318 if cont.end_headers {
1319 break
1320 }
1321 } else {
1322 return error('expected a CONTINUATION frame')
1323 }
1324 }
1325 }
1326 fields := c.decoder.decode(fragment)!
1327 mut s := c.lookup_stream(frame.stream_id)
1328 if s == unsafe { nil } {
1329 return c.check_unknown_stream(frame.stream_id)
1330 }
1331 s.mu.lock()
1332 // RFC 9113 §5.1: a HEADERS frame after the stream has already ended (a prior
1333 // END_STREAM, or RST/connection death that set s.ended) is a frame on a closed
1334 // stream, not more trailers. Without this, back-to-back trailer + extra HEADERS
1335 // in one read buffer would be appended to resp.headers before wait_response
1336 // flushes, delivering fields the peer sent after closing the stream. Reset
1337 // instead (the DATA path makes the same check via s.ended).
1338 if s.ended {
1339 s.mu.unlock()
1340 c.reset_stream(frame.stream_id, .stream_closed,
1341 'HEADERS frame after stream end on stream ${frame.stream_id}')
1342 return
1343 }
1344 was_headers_done := s.headers_done
1345 if !s.headers_done {
1346 mut status := 0
1347 mut status_valid := false
1348 for f in fields {
1349 if f.name == ':status' {
1350 // RFC 9113 §8.3.1: :status is exactly three digits. string.int()
1351 // is lenient ('200 OK' -> 200), so validate the raw value before
1352 // converting; otherwise a malformed status is accepted as valid.
1353 if f.value.len == 3 && all_digits(f.value) {
1354 status = f.value.int()
1355 status_valid = true
1356 }
1357 break
1358 }
1359 }
1360 // A response MUST carry a valid :status (RFC 9113 §8.3.1), and HTTP/2
1361 // forbids 101 (§8.1.1). A missing, malformed, out-of-range, or 101 status
1362 // is a stream-level PROTOCOL_ERROR — reset it rather than treat it as a
1363 // 1xx interim and wait forever for a "final" HEADERS that never arrives.
1364 // (Trailers legitimately omit :status, but they are handled below since
1365 // headers_done is already set by then.)
1366 if !status_valid || status < 100 || status > 599 || status == 101 {
1367 s.mu.unlock()
1368 c.reset_stream(frame.stream_id, .protocol_error,
1369 'response with a missing or invalid :status')
1370 return
1371 }
1372 // RFC 9110 §15.2 / RFC 9113 §8.1: a server may send 1xx interim responses
1373 // (100 Continue, 103 Early Hints) before the final response. They are not
1374 // the final response and carry no body, so ignore them and keep waiting
1375 // for the final (>= 200) HEADERS rather than latching the interim status
1376 // and headers — which would make the real final HEADERS look like trailers.
1377 if status >= 200 {
1378 s.status = status
1379 mut seen_regular := false
1380 mut seen_status := false
1381 for f in fields {
1382 if f.name.starts_with(':') {
1383 // RFC 9113 §8.3: the only valid response pseudo-header is :status
1384 // (consumed above); pseudo-headers MUST precede regular fields and
1385 // MUST NOT be duplicated. Any other ':' field, :status after a
1386 // regular field, or a second :status makes the response malformed.
1387 if f.name != ':status' || seen_regular || seen_status {
1388 s.mu.unlock()
1389 c.reset_stream(frame.stream_id, .protocol_error,
1390 'malformed response: invalid pseudo-header ${f.name}')
1391 return
1392 }
1393 seen_status = true
1394 continue
1395 }
1396 seen_regular = true
1397 // RFC 9113 §8.2: reject malformed field names (uppercase, empty) and
1398 // connection-specific fields rather than delivering them to the caller.
1399 reason := h2_response_field_error(f.name)
1400 if reason != '' {
1401 s.mu.unlock()
1402 c.reset_stream(frame.stream_id, .protocol_error,
1403 'malformed response: ${reason}')
1404 return
1405 }
1406 // RFC 9110 §8.6 / RFC 9113 §8.1.1: a malformed Content-Length makes
1407 // the message malformed (a stream-level PROTOCOL_ERROR). u64() is
1408 // lenient ('12junk' -> 12, '0x10' -> 16), so validate it strictly
1409 // here before wait_response trusts it for the body-length check.
1410 if f.name == 'content-length' && !all_digits(f.value) {
1411 s.mu.unlock()
1412 c.reset_stream(frame.stream_id, .protocol_error,
1413 'malformed Content-Length in response')
1414 return
1415 }
1416 s.resp_headers << f
1417 }
1418 s.headers_done = true
1419 }
1420 }
1421 // Trailers without END_STREAM violate RFC 7540 §8.1 (trailers must carry
1422 // END_STREAM); reset the stream rather than hanging wait_response forever.
1423 if was_headers_done && !frame.end_stream {
1424 s.mu.unlock()
1425 c.reset_stream(frame.stream_id, .protocol_error,
1426 'trailers HEADERS frame must carry END_STREAM')
1427 return
1428 }
1429 // A 1xx informational HEADERS with END_STREAM is also forbidden (RFC 9113 §8.1).
1430 if !s.headers_done && frame.end_stream {
1431 s.mu.unlock()
1432 c.reset_stream(frame.stream_id, .protocol_error, '1xx response must not carry END_STREAM')
1433 return
1434 }
1435 // A second HEADERS block on the stream carries trailers (RFC 9113 §8.1).
1436 // Preserve its non-pseudo fields (grpc-status, digest, ...) so callers on
1437 // the mux path keep the trailer metadata the synchronous
1438 // H2Conn.read_response surfaces. Pseudo-header fields are forbidden in
1439 // trailers (§8.1) and any malformed field name (§8.2) makes the message
1440 // malformed, so both reset the stream. wait_response flushes the kept fields
1441 // into resp.headers when the stream ends. (The trailers-without-END_STREAM
1442 // case was already reset above, so a trailer block here always carries
1443 // END_STREAM.)
1444 if was_headers_done {
1445 for f in fields {
1446 // RFC 9113 §8.1: trailers MUST NOT contain pseudo-header fields, and the
1447 // §8.2 field-name rules apply as for any header block.
1448 if f.name.starts_with(':') {
1449 s.mu.unlock()
1450 c.reset_stream(frame.stream_id, .protocol_error,
1451 'malformed trailers: pseudo-header ${f.name}')
1452 return
1453 }
1454 reason := h2_response_field_error(f.name)
1455 if reason != '' {
1456 s.mu.unlock()
1457 c.reset_stream(frame.stream_id, .protocol_error, 'malformed trailers: ${reason}')
1458 return
1459 }
1460 s.resp_trailers << f
1461 }
1462 }
1463 if frame.end_stream {
1464 s.ended = true
1465 }
1466 s.cv.signal()
1467 s.mu.unlock()
1468 if frame.end_stream {
1469 // The server closed the stream: wake any body sender that is blocked
1470 // waiting for flow-control credit, so it does not hang forever when
1471 // the peer withholds WINDOW_UPDATEs after an early final response.
1472 c.wake_send(mut s)
1473 }
1474}
1475
1476// on_response_data delivers a DATA payload to its stream, or — for recently
1477// cancelled/completed streams — keeps the connection-level flow-control
1478// account exact by returning the credit directly (mirroring the server's
1479// unknown-stream backstop).
1480fn (mut c H2MuxConn) on_response_data(frame H2DataFrame) ! {
1481 // Padding (the pad-length byte + padding) counts toward flow control
1482 // (RFC 7540 6.9.1) but is never delivered to the app, so credit it back
1483 // immediately; the data bytes are credited as the requester drains them.
1484 pad_overhead := frame.flow_size - frame.data.len
1485 // Enforce the connection-level receive window (RFC 7540 §6.9). Debit
1486 // first; credit paths (drain, unknown-stream, cancelled) restore it.
1487 // recv_wmu is always acquired solo, so no lock-order hazard here.
1488 c.recv_wmu.lock()
1489 c.conn_recv_window -= i64(frame.flow_size)
1490 conn_ok := c.conn_recv_window >= 0
1491 c.recv_wmu.unlock()
1492 if !conn_ok {
1493 return error('h2: peer exceeded connection-level receive window (RFC 7540 §6.9 FLOW_CONTROL_ERROR)')
1494 }
1495 mut s := c.lookup_stream(frame.stream_id)
1496 if s == unsafe { nil } {
1497 c.check_unknown_stream(frame.stream_id)!
1498 // Stream is gone: credit the whole received payload to the connection.
1499 if frame.flow_size > 0 {
1500 c.send_conn_window_update(u32(frame.flow_size)) or {}
1501 }
1502 return
1503 }
1504 s.mu.lock()
1505 if s.cancelled {
1506 // The requester abandoned this stream (it has been deregistered, but the
1507 // reader still held a reference). Account for the whole payload at the
1508 // connection level and drop it, so the window does not leak. Checking
1509 // the flag under s.mu — the same lock cancel_stream sets it under —
1510 // makes this race-free against a frame in flight during cancellation.
1511 s.mu.unlock()
1512 if frame.flow_size > 0 {
1513 c.send_conn_window_update(u32(frame.flow_size)) or {}
1514 }
1515 return
1516 }
1517 if !s.headers_done {
1518 // RFC 7540 §8.1: an HTTP/2 response must begin with HEADERS. DATA before
1519 // the final header block is malformed — RST this stream (PROTOCOL_ERROR)
1520 // rather than queue bytes a requester would see with status 0. Credit the
1521 // frame back to the connection window (debited above, never delivered).
1522 s.mu.unlock()
1523 if frame.flow_size > 0 {
1524 c.send_conn_window_update(u32(frame.flow_size)) or {}
1525 }
1526 c.reset_stream(frame.stream_id, .protocol_error,
1527 'DATA before response HEADERS on stream ${frame.stream_id}')
1528 return
1529 }
1530 // RFC 7540 §5.1: a DATA frame after the peer's END_STREAM is a stream-closed
1531 // error. If we have also closed our send side the stream is "closed" — a
1532 // connection error (the spec's MUST); otherwise it is "half-closed (remote)"
1533 // — a STREAM error, so one peer's misbehavior does not tear down every other
1534 // multiplexed stream. Checked before the flow-control debit: a half-closed
1535 // (remote) receiver is no longer obligated to maintain the window (§6.9.1).
1536 if s.ended {
1537 send_closed := s.send_closed
1538 s.mu.unlock()
1539 if send_closed {
1540 return error('h2: DATA frame after END_STREAM on closed stream ${frame.stream_id} (RFC 7540 §5.1)')
1541 }
1542 if frame.flow_size > 0 {
1543 c.send_conn_window_update(u32(frame.flow_size)) or {}
1544 }
1545 c.reset_stream(frame.stream_id, .stream_closed,
1546 'DATA after END_STREAM on stream ${frame.stream_id}')
1547 return
1548 }
1549 // Enforce the stream-level receive window (RFC 7540 §6.9.1). Padding
1550 // counts against stream flow control the same as data, so debit
1551 // whenever flow_size > 0 regardless of whether there are data bytes.
1552 if frame.flow_size > 0 {
1553 s.recv_window -= i64(frame.flow_size)
1554 if s.recv_window < 0 {
1555 s.mu.unlock()
1556 // RFC 7540 §6.9.1: a stream-level flow-control violation is a STREAM
1557 // error — RST just this stream (like the WINDOW_UPDATE overflow path)
1558 // rather than failing the whole connection and every other multiplexed
1559 // stream. Credit this frame's bytes back to the connection window
1560 // (debited above, never delivered); reset_stream credits queued chunks.
1561 c.send_conn_window_update(u32(frame.flow_size)) or {}
1562 c.reset_stream(frame.stream_id, .flow_control_error,
1563 'peer exceeded stream ${frame.stream_id} receive window')
1564 return
1565 }
1566 }
1567 if frame.data.len > 0 {
1568 s.chunks << frame.data.clone()
1569 s.body_rcvd += u64(frame.data.len)
1570 }
1571 if frame.end_stream {
1572 s.ended = true
1573 }
1574 s.cv.signal()
1575 s.mu.unlock()
1576 if pad_overhead > 0 {
1577 c.send_window_updates(s.id, u32(pad_overhead)) or {}
1578 }
1579 if frame.end_stream {
1580 // The response ended on this DATA frame (an early final response with a
1581 // body, e.g. a 413/auth rejection). Wake any body sender still blocked on
1582 // flow-control credit, mirroring on_response_headers; otherwise it hangs
1583 // when the peer withholds further WINDOW_UPDATEs.
1584 c.wake_send(mut s)
1585 }
1586}
1587
1588// check_unknown_stream distinguishes late frames for cancelled/finished
1589// streams (fine) from protocol garbage (connection error).
1590fn (mut c H2MuxConn) check_unknown_stream(stream_id u32) ! {
1591 c.wmu.lock()
1592 known_range := stream_id < c.next_stream_id
1593 c.wmu.unlock()
1594 if stream_id % 2 == 1 && known_range {
1595 return
1596 }
1597 return error('frame for an unknown stream ${stream_id}')
1598}
1599
1600// wake_send marks a stream's send side dead and wakes a body sender that is
1601// blocked waiting for flow-control credit, so a RST_STREAM or GOAWAY terminates
1602// the upload instead of hanging it. The caller must hold no connection lock
1603// (it takes fmu). fail() must already have recorded the terminal error.
1604fn (mut c H2MuxConn) wake_send(mut s H2MuxStream) {
1605 c.fmu.lock()
1606 s.send_dead = true
1607 c.fcv.broadcast()
1608 c.fmu.unlock()
1609}
1610
1611// reset_stream deregisters a stream, sends RST_STREAM(code), and fails its
1612// requester — used by the reader for a stream-level protocol/flow-control error.
1613// Deregistering before the RST lets any in-flight late DATA hit the
1614// unknown-stream backstop (which credits the connection window).
1615fn (mut c H2MuxConn) reset_stream(stream_id u32, code H2ErrorCode, reason string) {
1616 c.smu.lock()
1617 mut s := c.streams[stream_id] or { &H2MuxStream(unsafe { nil }) }
1618 c.streams.delete(stream_id)
1619 c.smu.unlock()
1620 c.wmu.lock()
1621 c.write_all_locked(H2Frame(H2RstStreamFrame{
1622 stream_id: stream_id
1623 error_code: u32(code)
1624 }).encode()) or {
1625 c.wmu.unlock()
1626 c.note_write_failure()
1627 // The stream was removed from c.streams above, so fail_conn() will not
1628 // find it. Wake it directly so the requester does not block forever.
1629 if s != unsafe { nil } {
1630 s.fail('h2: transport write failure', false)
1631 c.wake_send(mut s)
1632 }
1633 return
1634 }
1635 c.wmu.unlock()
1636 if s != unsafe { nil } {
1637 // Credit back any DATA bytes already queued but never drained,
1638 // mirroring cancel_stream, so the connection receive window stays
1639 // accurate after a per-stream reset.
1640 s.mu.lock()
1641 mut queued := u64(0)
1642 for ch in s.chunks {
1643 queued += u64(ch.len)
1644 }
1645 s.chunks.clear()
1646 s.mu.unlock()
1647 if queued > 0 {
1648 c.send_conn_window_update(u32(queued)) or {}
1649 }
1650 s.fail('h2: ${reason}', false)
1651 c.wake_send(mut s)
1652 }
1653}
1654
1655fn (mut c H2MuxConn) lookup_stream(stream_id u32) &H2MuxStream {
1656 c.smu.lock()
1657 defer {
1658 c.smu.unlock()
1659 }
1660 if s := c.streams[stream_id] {
1661 return s
1662 }
1663 return &H2MuxStream(unsafe { nil })
1664}
1665
1666// fail_conn marks the connection dead, fails every in-flight stream, and wakes
1667// all blocked senders. Normally called by the reader on transport error; also
1668// called by note_write_failure when close_transport is nil and cannot interrupt
1669// the reader. Guards against double-invocation via the c.closed check under smu.
1670// Teardown is routed through teardown_transport, which runs close_transport
1671// exactly once, so this path and drop_ref's last-reference teardown can never
1672// double-close.
1673fn (mut c H2MuxConn) fail_conn(msg string) {
1674 c.smu.lock()
1675 if c.closed {
1676 c.smu.unlock()
1677 return
1678 }
1679 c.closed = true
1680 c.conn_err = msg
1681 mut open := []&H2MuxStream{}
1682 for _, s in c.streams {
1683 open << s
1684 }
1685 c.streams.clear()
1686 pending_teardown := c.refs <= 0
1687 c.smu.unlock()
1688 for mut s in open {
1689 s.mu.lock()
1690 retryable := !s.sent_headers
1691 s.mu.unlock()
1692 s.fail(msg, retryable)
1693 }
1694 c.fmu.lock()
1695 c.fmu_dead = true
1696 c.fcv.broadcast()
1697 c.fmu.unlock()
1698 if pending_teardown {
1699 c.teardown_transport()
1700 }
1701}
1702