Skip to content

Commit f541b52

Browse files
net.http: honor peer SETTINGS_MAX_HEADER_LIST_SIZE on requests (RFC 9113 §6.5.2)
The sync and server paths recorded the peer advisory SETTINGS_MAX_HEADER_LIST_SIZE but neither client path enforced it, and the mux path did not even store it (apply_peer_settings had no arm for it, so it was silently ignored). A request whose header list exceeded the server limit was emitted and rejected only after the round trip. Store the setting on the mux path (new wmu-guarded field + apply_peer_settings arm, matching the sync/server paths) and, on both client paths, compute the §6.5.2 header-list size (sum of name+value+32 per field) before encoding and refuse an over-limit request locally with a non-retryable error (a fresh connection to the same peer carries the same limit). Default is unlimited, so behavior is unchanged unless the peer sets a limit. Adds tests on both paths. Co-Authored-By: WOZCODE <contact@withwoz.com>
1 parent c53bd09 commit f541b52

4 files changed

Lines changed: 106 additions & 6 deletions

File tree

vlib/net/http/h2_conn.v

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,15 @@ pub fn (mut c H2Conn) do(req H2ClientRequest) !H2ClientResponse {
151151
for h in req.headers {
152152
fields << h
153153
}
154+
// RFC 9113 §6.5.2: honor the peer's advisory SETTINGS_MAX_HEADER_LIST_SIZE —
155+
// refuse an over-limit request locally instead of having the server reject it
156+
// after the round trip.
157+
if c.peer.max_header_list_size != max_u32 {
158+
size := h2_header_list_size(fields)
159+
if size > u64(c.peer.max_header_list_size) {
160+
return error('h2: request header list (${size} bytes) exceeds peer SETTINGS_MAX_HEADER_LIST_SIZE (${c.peer.max_header_list_size})')
161+
}
162+
}
154163
block := c.encoder.encode(fields)
155164
has_body := req.body.len > 0
156165
c.send_header_block(stream_id, block, !has_body)!

vlib/net/http/h2_conn_test.v

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -576,3 +576,25 @@ fn test_h2_conn_rejects_malformed_response_fields() {
576576
assert err.msg().contains('uppercase'), 'unexpected error: ${err.msg()}'
577577
}
578578
}
579+
580+
// RFC 9113 §6.5.2: the client honors the peer's advisory
581+
// SETTINGS_MAX_HEADER_LIST_SIZE and refuses an over-limit request rather than
582+
// emitting it. Conformance gap G4 (set white-box: on the sync path the peer's
583+
// SETTINGS arrive only with the response, after the request is built).
584+
fn test_h2_conn_respects_peer_max_header_list_size() {
585+
mut c := new_h2_conn(&MockTransport{
586+
inbound: build_server_stream([H2HeaderField{':status', '200'}], [])
587+
})
588+
c.peer.max_header_list_size = 40 // tiny: even the pseudo-headers exceed it
589+
if _ := c.do(H2ClientRequest{
590+
method: 'GET'
591+
scheme: 'https'
592+
authority: 'example.com'
593+
path: '/a-fairly-long-path-to-exceed-the-limit'
594+
})
595+
{
596+
assert false, 'over-limit request was sent'
597+
} else {
598+
assert err.msg().contains('MAX_HEADER_LIST_SIZE'), 'unexpected error: ${err.msg()}'
599+
}
600+
}

vlib/net/http/h2_mux_conn.v

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,17 @@ fn h2_response_field_error(name string) string {
9797
return ''
9898
}
9999

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.
103+
fn 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+
100111
// H2MuxStream is the client-side state of one in-flight request stream.
101112
@[heap]
102113
struct H2MuxStream {
@@ -157,12 +168,13 @@ mut:
157168
// panics on a nil closer.
158169
close_transport fn () = unsafe { nil }
159170
// --- guarded by wmu ---
160-
wmu &sync.Mutex = sync.new_mutex()
161-
encoder H2HpackEncoder
162-
next_stream_id u32 = 1
163-
handshaked bool
164-
pending_settings_acks int // count of SETTINGS frames received before our preface; each needs one ACK
165-
pending_ping_acks [][]u8 // PING frames received before our preface; each needs an ACK with the same data
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)
166178
// --- guarded by fmu, fcv signals growth/death ---
167179
fmu &sync.Mutex = unsafe { nil }
168180
fcv &sync.Cond = unsafe { nil }
@@ -379,6 +391,15 @@ fn (mut c H2MuxConn) do_on_stream(mut s H2MuxStream, req H2ClientRequest) !H2Cli
379391
c.note_write_failure()
380392
return h2_retryable_error('connection handshake failed: ${err.msg()}')
381393
}
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+
}
382403
if c.next_stream_id > u32(0x7fff_ffff) {
383404
// RFC 7540 §5.1.1: client stream IDs are odd and must not exceed 2^31-1.
384405
// Retire this connection and let the caller open a fresh one.
@@ -1205,6 +1226,15 @@ fn (mut c H2MuxConn) apply_peer_settings(settings []H2Setting) ! {
12051226
c.peer_max_streams = st.value
12061227
c.smu.unlock()
12071228
}
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+
}
12081238
h2_settings_initial_window_size {
12091239
if st.value > u32(0x7fff_ffff) {
12101240
// RFC 7540 6.5.3: values above 2^31-1 are a FLOW_CONTROL_ERROR;

vlib/net/http/h2_mux_conn_test.v

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2082,3 +2082,42 @@ fn test_mux_trailers_reject_pseudo() {
20822082
assert peer.failure_msg() == ''
20832083
assert got.contains('malformed trailers'), 'trailers pseudo-header not rejected: ${got}'
20842084
}
2085+
2086+
// RFC 9113 §6.5.2: the client honors the peer's advisory
2087+
// SETTINGS_MAX_HEADER_LIST_SIZE and refuses an over-limit request locally rather
2088+
// than emitting it. Conformance gap G4. The peer sends no SETTINGS here, so the
2089+
// reader never writes peer_max_header_list_size — setting it directly before the
2090+
// request is race-free.
2091+
fn test_mux_request_respects_peer_max_header_list_size() {
2092+
mut cend, mut pend := new_mux_pipe()
2093+
mut conn := new_test_mux_conn(mut cend)
2094+
conn.peer_max_header_list_size = 40 // tiny: even the pseudo-headers exceed it
2095+
mut peer := &MuxTestPeer{
2096+
end: pend
2097+
}
2098+
peer_thread := spawn fn (mut peer MuxTestPeer) {
2099+
peer.read_preface() or {
2100+
peer.fail('preface: ${err.msg()}')
2101+
return
2102+
}
2103+
for {
2104+
peer.pump() or { return }
2105+
}
2106+
}(mut peer)
2107+
mut got := ''
2108+
if _ := conn.do(H2ClientRequest{
2109+
method: 'GET'
2110+
scheme: 'https'
2111+
authority: 'example.com'
2112+
path: '/a-fairly-long-path-to-exceed-the-limit'
2113+
})
2114+
{
2115+
got = '<<accepted>>'
2116+
} else {
2117+
got = err.msg()
2118+
}
2119+
cend.close_both()
2120+
peer_thread.wait()
2121+
assert peer.failure_msg() == ''
2122+
assert got.contains('MAX_HEADER_LIST_SIZE'), 'over-limit request not rejected: ${got}'
2123+
}

0 commit comments

Comments
 (0)