From: Willy Tarreau Date: Mon, 9 Nov 2026 17:53:21 +0000 (+0000) Subject: BUG/MEDIUM: mux-h2: also emit the HPACK dynamic table size update on requests X-Git-Url: https://git.kaiwu.me/$%7BGITURL%7D/1?a=commitdiff_plain;h=e8eea22d0d0f1f9d96e5083a72d728c8fe5f2974;p=haproxy.git BUG/MEDIUM: mux-h2: also emit the HPACK dynamic table size update on requests Commit 39a0a1e12 ("MEDIUM: h2/hpack: emit a Dynamic Table Size Update after settings change") made the mux emit an HPACK Dynamic Table Size Update at the beginning of the first header block following a change of the peer's SETTINGS_HEADER_TABLE_SIZE (RFC7541#4.2), but only in h2s_snd_fhdrs(), i.e. for responses sent to clients. H2_CF_SHTS_UPDATED is also set on backend connections, but h2s_snd_bhdrs() never emits the update. Indeed, by then the spec was still hesitant on this point and we were not certain that this behavior would be kept, and even less that it would be necessary at all for the backend side. On the backend side this is almost certain to happen with servers which advertise a lower header table size: the first request is usually sent before the server's SETTINGS frame is received, and the next one is the first header block after the change. Strict decoders (e.g. those based on nghttp2 or python-h2) treat the missing update as a COMPRESSION_ERROR on the first request sent after the server's SETTINGS was acknowledged, which kills the connection. This is reproducible with a python-h2 server announcing a lower header table size: before the fix, the second request on the connection triggers a COMPRESSION_ERROR ("Encoder did not shrink table size to within the max") and fails, and a new connection is used for the third one; after the fix, all requests are served over the same connection. The update is now also emitted at the beginning of request HEADERS frames, exactly as for responses on the frontend size, and the flags are only updated once the frame is committed, as in h2s_snd_fhdrs(). Emitting a DTSU toward servers which did not expect one could be a concern since some implementations were already met which do not like receiving one (VTest in the case of the commit above). It is considered safe here for the following reasons: - the update is only emitted after the server explicitly advertised SETTINGS_HEADER_TABLE_SIZE in its SETTINGS frame. Servers which do not advertise it will never see it, which is the same rule that has been applied to clients since 2.6. - nghttp2's encoder, hence curl, nghttpx, Apache's mod_proxy_http2 and every other client based on it, already does exactly this: on any SETTINGS_HEADER_TABLE_SIZE received, including one which does not change the value, it starts the next header block with an update (nghttp2_hd_deflate_change_table_size() unconditionally sets notify_table_size_change). This was verified on the wire with curl 8.5.0 and nghttp2 1.59: a server advertising 4096 or 65536 receives an update to 4096 at the beginning of the next request. Thus any server advertising this setting is already routinely exposed to this opcode. - the only difference is the value: we always send zero since we don't use the dynamic table, while nghttp2 sends min(advertised, 4096). Zero is always valid since RFC7541#6.3 only requires the value not to exceed the advertised limit, and we've been sending this exact update to clients (including browsers) over the last 5 years. This could be backported to all versions, but only if found necessary. It would make sense to have it in 3.4 and 3.3 though. --- diff --git a/src/mux_h2.c b/src/mux_h2.c index bca78bc85..762d53f65 100644 --- a/src/mux_h2.c +++ b/src/mux_h2.c @@ -7238,6 +7238,18 @@ static size_t h2s_snd_bhdrs(struct h2s *h2s, struct htx *htx) write_n32(outbuf.area + 5, h2s->id); // 4 bytes outbuf.data = 9; + if ((h2c->flags & (H2_CF_SHTS_UPDATED|H2_CF_DTSU_EMITTED)) == H2_CF_SHTS_UPDATED) { + /* SETTINGS_HEADER_TABLE_SIZE changed, we must send an HPACK + * dynamic table size update at the beginning of the next + * header block. See h2s_snd_fhdrs() for more details. + */ + if (b_room(&outbuf) < 1) + goto full; + outbuf.area[outbuf.data++] = 0x20; // HPACK DTSU 0 bytes + + /* flags will only be updated once the buffer is committed */ + } + /* encode the method, which necessarily is the first one */ if (!hpack_encode_method(&outbuf, sl->info.req.meth, meth)) { if (b_space_wraps(mbuf)) @@ -7480,6 +7492,13 @@ static size_t h2s_snd_bhdrs(struct h2s *h2s, struct htx *htx) /* commit the H2 response */ b_add(mbuf, outbuf.data); h2c->flags |= H2_CF_MBUF_HAS_DATA; + + if (h2c->flags & H2_CF_SHTS_UPDATED) { + /* was sent above */ + h2c->flags |= H2_CF_DTSU_EMITTED; + h2c->flags &= ~H2_CF_SHTS_UPDATED; + } + h2s->flags |= H2_SF_HEADERS_SENT; h2s->st = H2_SS_OPEN;