From 573f911331e56813745f7b03a319ef8f5e953044 Mon Sep 17 00:00:00 2001 From: Hiroshi Hatake Date: Fri, 16 Oct 2026 16:55:09 +0900 Subject: [PATCH 1/6] http_server: bound decoded HTTP/2 request headers Signed-off-by: Hiroshi Hatake --- include/fluent-bit/flb_http_common.h | 3 ++ .../http_server/flb_http_server_http2.h | 4 +++ src/flb_http_common.c | 2 ++ src/http_server/flb_http_server_http2.c | 31 +++++++++++++++++-- 4 files changed, 38 insertions(+), 2 deletions(-) diff --git a/include/fluent-bit/flb_http_common.h b/include/fluent-bit/flb_http_common.h index 01032a4b025..ab7b92cfd98 100644 --- a/include/fluent-bit/flb_http_common.h +++ b/include/fluent-bit/flb_http_common.h @@ -131,6 +131,9 @@ struct flb_http_stream { int role; int status; + size_t incoming_header_size; + size_t incoming_header_count; + struct flb_http_request request; struct flb_http_response response; diff --git a/include/fluent-bit/http_server/flb_http_server_http2.h b/include/fluent-bit/http_server/flb_http_server_http2.h index 2c94d34251f..7bd67a4d708 100644 --- a/include/fluent-bit/http_server/flb_http_server_http2.h +++ b/include/fluent-bit/http_server/flb_http_server_http2.h @@ -24,6 +24,10 @@ #include +/* Decoded header-list budgets per stream, including pseudo-headers and trailers. */ +#define FLB_HTTP2_MAX_HEADER_LIST_SIZE 65536 +#define FLB_HTTP2_MAX_HEADER_COUNT 100 + struct flb_http_server_session; struct flb_http_stream; diff --git a/src/flb_http_common.c b/src/flb_http_common.c index 7449d723ddf..85d29bbb913 100644 --- a/src/flb_http_common.c +++ b/src/flb_http_common.c @@ -1436,6 +1436,8 @@ int flb_http_stream_init(struct flb_http_stream *stream, int result; stream->id = stream_id; + stream->incoming_header_size = 0; + stream->incoming_header_count = 0; if (role == HTTP_STREAM_ROLE_SERVER) { stream->status = HTTP_STREAM_STATUS_RECEIVING_HEADERS; diff --git a/src/http_server/flb_http_server_http2.c b/src/http_server/flb_http_server_http2.c index 218daf028ec..0132a77f504 100644 --- a/src/http_server/flb_http_server_http2.c +++ b/src/http_server/flb_http_server_http2.c @@ -393,7 +393,7 @@ void flb_http2_server_stream_release(struct flb_http_stream *stream) int flb_http2_server_session_init(struct flb_http2_server_session *session, struct flb_http_server_session *parent) { - nghttp2_settings_entry session_settings[1]; + nghttp2_settings_entry session_settings[2]; nghttp2_session_callbacks *callbacks; int result; @@ -432,10 +432,13 @@ int flb_http2_server_session_init(struct flb_http2_server_session *session, session_settings[0].settings_id = NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS; session_settings[0].value = 1; + session_settings[1].settings_id = NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE; + session_settings[1].value = FLB_HTTP2_MAX_HEADER_LIST_SIZE; + result = nghttp2_submit_settings(session->inner_session, NGHTTP2_FLAG_NONE, session_settings, - 1); + 2); if (result != 0) { return -3; @@ -547,6 +550,30 @@ static int http2_header_callback(nghttp2_session *inner_session, return 0; } + /* + * SETTINGS_MAX_HEADER_LIST_SIZE is advisory. Enforce the decoded budget + * before any copies, including replacements and trailer fields. RFC 9113 + * counts 32 bytes of overhead per field. Subtract to avoid size overflow. + */ + if (stream->incoming_header_count >= FLB_HTTP2_MAX_HEADER_COUNT || + name_length > FLB_HTTP2_MAX_HEADER_LIST_SIZE - 32 || + value_length > FLB_HTTP2_MAX_HEADER_LIST_SIZE - 32 - name_length || + stream->incoming_header_size > + FLB_HTTP2_MAX_HEADER_LIST_SIZE - 32 - name_length - value_length) { + stream->status = HTTP_STREAM_STATUS_RELEASED; + + result = nghttp2_submit_rst_stream(inner_session, NGHTTP2_FLAG_NONE, + frame->hd.stream_id, NGHTTP2_ENHANCE_YOUR_CALM); + if (result != 0) { + return NGHTTP2_ERR_CALLBACK_FAILURE; + } + + return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; + } + + stream->incoming_header_count++; + stream->incoming_header_size += name_length + value_length + 32; + if (flb_http_server_strncasecmp(name, name_length, ":method", 0) == 0) { strncpy(temporary_buffer, (const char *) value, From 1163f08d7962c53989781c257476692e255f9c78 Mon Sep 17 00:00:00 2001 From: Hiroshi Hatake Date: Fri, 16 Oct 2026 16:55:09 +0900 Subject: [PATCH 2/6] tests: integration: cover HTTP/2 header budgets Signed-off-by: Hiroshi Hatake --- .../in_http/tests/test_http2_header_limits.py | 194 ++++++++++++++++++ 1 file changed, 194 insertions(+) create mode 100644 tests/integration/scenarios/in_http/tests/test_http2_header_limits.py diff --git a/tests/integration/scenarios/in_http/tests/test_http2_header_limits.py b/tests/integration/scenarios/in_http/tests/test_http2_header_limits.py new file mode 100644 index 00000000000..bf9846c05aa --- /dev/null +++ b/tests/integration/scenarios/in_http/tests/test_http2_header_limits.py @@ -0,0 +1,194 @@ +"""Bound decoded HTTP/2 headers, including indexed fields and trailers.""" + +import http.client +import socket +import ssl +from pathlib import Path + +import pytest +from hpack import Decoder, Encoder +from hyperframe.frame import Frame, HeadersFrame, ContinuationFrame, SettingsFrame + +from utils.test_service import FluentBitTestService + + +MAX_BYTES = 65536 +MAX_FIELDS = 100 +BASE_HEADERS = [(b":method", b"GET"), (b":scheme", b"http"), + (b":authority", b"localhost"), (b":path", b"/health")] +# XXH3_64bits("x-collision-N") % 16 == 0 for these suffixes. +COLLISIONS = [ + 2, 12, 17, 20, 39, 54, 96, 146, 163, 198, 270, 278, 296, 314, 368, 380, + 387, 394, 418, 428, 446, 458, 484, 485, 489, 490, 496, 502, 509, 524, + 526, 531, 546, 550, 585, 658, 671, 676, 697, 707, 727, 740, 765, 766, + 778, 783, 800, 853, 863, 898, 909, 910, 934, 954, 958, 969, 994, 996, + 1007, 1015, 1030, 1043, 1047, 1068, 1079, 1097, 1111, 1112, 1138, + 1148, 1166, 1183, 1210, 1213, 1229, 1241, 1250, 1251, 1268, 1285, + 1297, 1303, 1334, 1390, 1424, 1463, 1483, 1486, 1533, 1558, 1567, + 1582, 1591, 1647, 1651, 1661, 1666, 1671, 1683, 1687, +] + + +class Client: + def __init__(self, port, tls): + self.socket = socket.create_connection(("127.0.0.1", port), timeout=5) + if tls: + context = ssl._create_unverified_context() + context.set_alpn_protocols(["h2"]) + self.socket = context.wrap_socket(self.socket, server_hostname="localhost") + assert self.socket.selected_alpn_protocol() == "h2" + self.encoder = Encoder() + self.decoder = Decoder() + self.pending = b"" + self.settings = {} + self.socket.sendall(b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n" + SettingsFrame().serialize()) + + def headers(self, stream, headers, end=True): + # Bypass client-side advisory limits; keep valid frame sizes. + block = self.encoder.encode(headers, huffman=False) + for offset in range(0, len(block), 16384): + frame = HeadersFrame(stream) if offset == 0 else ContinuationFrame(stream) + frame.data = block[offset:offset + 16384] + if offset + 16384 >= len(block): + frame.flags.add("END_HEADERS") + if offset == 0 and end: + frame.flags.add("END_STREAM") + self.socket.sendall(frame.serialize()) + return len(block) + + def result(self, stream): + while True: + while len(self.pending) < 9: + data = self.socket.recv(65536) + assert data, "connection closed before stream result" + self.pending += data + frame, length = Frame.parse_frame_header(memoryview(self.pending[:9])) + while len(self.pending) < 9 + length: + data = self.socket.recv(65536) + assert data, "connection closed during frame" + self.pending += data + frame.parse_body(memoryview(self.pending[9:9 + length])) + self.pending = self.pending[9 + length:] + if frame.type == 4 and "ACK" not in frame.flags: + self.settings.update(frame.settings) + self.socket.sendall(SettingsFrame(flags=["ACK"]).serialize()) + if frame.stream_id == stream and frame.type in (1, 3): + if frame.type == 1: + assert (":status", "200") in self.decoder.decode(frame.data) + return frame + assert frame.type != 7, "unexpected connection-level GOAWAY" + + +@pytest.fixture(params=[False, True], ids=["cleartext", "tls"]) +def listener(request, tmp_path): + cert_dir = Path(__file__).resolve().parents[2] / "in_splunk" / "certificate" + config = tmp_path / "headers.yaml" + config.write_text("""service: + flush: 1 + http_server: on + http_port: ${FLUENT_BIT_HTTP_MONITORING_PORT} +pipeline: + inputs: + - name: http + listen: 127.0.0.1 + port: ${FLUENT_BIT_TEST_LISTENER_PORT} + enable_health_endpoint: on + http2: on + tls: ${HEADER_TEST_TLS} + tls.crt_file: ${CERTIFICATE_TEST} + tls.key_file: ${PRIVATE_KEY_TEST} + outputs: + - name: null + match: '*' +""") + service = FluentBitTestService(str(config), extra_env={ + "HEADER_TEST_TLS": "on" if request.param else "off", + "CERTIFICATE_TEST": str(cert_dir / "certificate.pem"), + "PRIVATE_KEY_TEST": str(cert_dir / "private_key.pem"), + }) + service.start() + try: + yield service, request.param + finally: + service.stop() + + +@pytest.mark.parametrize("case,rejected", [ + ("count_exact", False), ("count_over", True), + ("bytes_exact", False), ("bytes_over", True), + ("indexed", True), ("unique", True), + ("collisions_exact", False), ("collisions_over", True), + ("path_over", True), ("name_over", True), ("value_over", True), + ("trailers_count_exact", False), ("trailers_count_over", True), + ("trailers_bytes_exact", False), ("trailers_bytes_over", True), +]) +def test_http2_header_budget(listener, case, rejected): + service, tls = listener + client = Client(service.flb_listener_port, tls) + headers = list(BASE_HEADERS) + if tls: + headers[1] = (b":scheme", b"https") + base_size = sum(len(k) + len(v) + 32 for k, v in headers) + trailers = None + if case.startswith("collisions"): + count = MAX_FIELDS - len(headers) + case.endswith("over") + headers += [(f"x-collision-{i}".encode(), b"v") for i in COLLISIONS[:count]] + elif case == "path_over": + headers[3] = (b":path", b"/" + b"a" * (MAX_BYTES - 1)) + elif case == "name_over": + headers += [(b"x" * MAX_BYTES, b"v")] + elif case == "value_over": + headers += [(b"x-pad", b"v" * MAX_BYTES)] + elif "count" in case: + fields = [(b"x-repeat", b"a")] * (MAX_FIELDS - len(headers) + case.endswith("over")) + if case.startswith("trailers"): + headers += fields[:40] + trailers = fields[40:] + else: + headers += fields + elif "bytes" in case: + value_size = MAX_BYTES - base_size - 32 - len(b"x-pad") + case.endswith("over") + if case.startswith("trailers"): + headers += [(b"x-pad", b"a" * 30000)] + trailers = [(b"x-pad", b"b" * (value_size - 30000 - 37))] + else: + headers += [(b"x-pad", b"a" * value_size)] + elif case == "indexed": + headers += [(b"x-repeat", b"a" * 4000)] * 200 + else: + headers += [(f"x-unique-{i}".encode(), b"v") for i in range(200)] + try: + wire_bytes = client.headers(1, headers, end=trailers is None) + if case == "indexed": + assert wire_bytes < 4500 # Over 800 KB decoded from a small header block. + if trailers is not None: + client.headers(1, trailers) + result = client.result(1) + assert result.type == (3 if rejected else 1) + assert client.settings[SettingsFrame.MAX_HEADER_LIST_SIZE] == MAX_BYTES + if rejected: + assert result.error_code == 11 # ENHANCE_YOUR_CALM + # Rejection releases the stream and preserves the connection. + client.headers(3, BASE_HEADERS) + assert client.result(3).type == 1 + assert service.flb.process.poll() is None + finally: + client.socket.close() + + +def test_http1_health_with_http2_enabled(listener): + service, tls = listener + if tls: + connection = http.client.HTTPSConnection( + "localhost", service.flb_listener_port, timeout=5, + context=ssl._create_unverified_context(), + ) + else: + connection = http.client.HTTPConnection("localhost", service.flb_listener_port, timeout=5) + try: + connection.request("GET", "/health") + response = connection.getresponse() + assert response.status == 200 + response.read() + finally: + connection.close() From 8bb73af1f64e075012f5b71d1a619cd44ff1afe2 Mon Sep 17 00:00:00 2001 From: Hiroshi Hatake Date: Fri, 16 Oct 2026 18:22:39 +0900 Subject: [PATCH 3/6] http_server: reject invalid HTTP/2 header fields Signed-off-by: Hiroshi Hatake --- src/http_server/flb_http_server_http2.c | 86 ++++++++++++++++++++----- 1 file changed, 69 insertions(+), 17 deletions(-) diff --git a/src/http_server/flb_http_server_http2.c b/src/http_server/flb_http_server_http2.c index 0132a77f504..fb2ce38709b 100644 --- a/src/http_server/flb_http_server_http2.c +++ b/src/http_server/flb_http_server_http2.c @@ -37,6 +37,12 @@ static int http2_header_callback(nghttp2_session *inner_session, uint8_t flags, void *user_data); +static int http2_invalid_header_callback(nghttp2_session *inner_session, + const nghttp2_frame *frame, + const uint8_t *name, size_t name_length, + const uint8_t *value, size_t value_length, + uint8_t flags, void *user_data); + static int http2_frame_recv_callback(nghttp2_session *inner_session, const nghttp2_frame *frame, void *user_data); @@ -420,6 +426,8 @@ int flb_http2_server_session_init(struct flb_http2_server_session *session, nghttp2_session_callbacks_set_on_data_chunk_recv_callback(callbacks, http2_data_chunk_recv_callback); nghttp2_session_callbacks_set_on_header_callback(callbacks, http2_header_callback); + nghttp2_session_callbacks_set_on_invalid_header_callback( + callbacks, http2_invalid_header_callback); result = nghttp2_session_server_new(&session->inner_session, callbacks, session); @@ -524,6 +532,64 @@ static ssize_t http2_send_callback(nghttp2_session *inner_session, return length; } +static int http2_account_header(nghttp2_session *inner_session, + const nghttp2_frame *frame, + struct flb_http_stream *stream, + size_t name_length, size_t value_length) +{ + int result; + + if (stream->incoming_header_count >= FLB_HTTP2_MAX_HEADER_COUNT || + name_length > FLB_HTTP2_MAX_HEADER_LIST_SIZE - 32 || + value_length > FLB_HTTP2_MAX_HEADER_LIST_SIZE - 32 - name_length || + stream->incoming_header_size > + FLB_HTTP2_MAX_HEADER_LIST_SIZE - 32 - name_length - value_length) { + stream->status = HTTP_STREAM_STATUS_RELEASED; + + result = nghttp2_submit_rst_stream(inner_session, NGHTTP2_FLAG_NONE, + frame->hd.stream_id, NGHTTP2_ENHANCE_YOUR_CALM); + if (result != 0) { + return NGHTTP2_ERR_CALLBACK_FAILURE; + } + + return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; + } + + stream->incoming_header_count++; + stream->incoming_header_size += name_length + value_length + 32; + + return 0; +} + +static int http2_invalid_header_callback(nghttp2_session *inner_session, + const nghttp2_frame *frame, + const uint8_t *name, size_t name_length, + const uint8_t *value, size_t value_length, + uint8_t flags, void *user_data) +{ + struct flb_http_stream *stream; + int result; + + stream = nghttp2_session_get_stream_user_data(inner_session, frame->hd.stream_id); + if (stream == NULL) { + return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; + } + + if (stream->status != HTTP_STREAM_STATUS_RECEIVING_HEADERS && + stream->status != HTTP_STREAM_STATUS_RECEIVING_DATA) { + return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; + } + + result = http2_account_header(inner_session, frame, stream, name_length, value_length); + if (result != 0) { + return result; + } + + /* Reject invalid fields on every nghttp2 version, including 1.67+. */ + stream->status = HTTP_STREAM_STATUS_RELEASED; + return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; +} + static int http2_header_callback(nghttp2_session *inner_session, const nghttp2_frame *frame, const uint8_t *name, @@ -555,25 +621,11 @@ static int http2_header_callback(nghttp2_session *inner_session, * before any copies, including replacements and trailer fields. RFC 9113 * counts 32 bytes of overhead per field. Subtract to avoid size overflow. */ - if (stream->incoming_header_count >= FLB_HTTP2_MAX_HEADER_COUNT || - name_length > FLB_HTTP2_MAX_HEADER_LIST_SIZE - 32 || - value_length > FLB_HTTP2_MAX_HEADER_LIST_SIZE - 32 - name_length || - stream->incoming_header_size > - FLB_HTTP2_MAX_HEADER_LIST_SIZE - 32 - name_length - value_length) { - stream->status = HTTP_STREAM_STATUS_RELEASED; - - result = nghttp2_submit_rst_stream(inner_session, NGHTTP2_FLAG_NONE, - frame->hd.stream_id, NGHTTP2_ENHANCE_YOUR_CALM); - if (result != 0) { - return NGHTTP2_ERR_CALLBACK_FAILURE; - } - - return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; + result = http2_account_header(inner_session, frame, stream, name_length, value_length); + if (result != 0) { + return result; } - stream->incoming_header_count++; - stream->incoming_header_size += name_length + value_length + 32; - if (flb_http_server_strncasecmp(name, name_length, ":method", 0) == 0) { strncpy(temporary_buffer, (const char *) value, From 44ce53afe4f13b05a43cbb56df3f9de749518410 Mon Sep 17 00:00:00 2001 From: Hiroshi Hatake Date: Fri, 16 Oct 2026 18:22:39 +0900 Subject: [PATCH 4/6] tests: integration: cover invalid HTTP/2 fields Signed-off-by: Hiroshi Hatake --- .../in_http/tests/test_http2_header_limits.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/integration/scenarios/in_http/tests/test_http2_header_limits.py b/tests/integration/scenarios/in_http/tests/test_http2_header_limits.py index bf9846c05aa..d7a364b49d3 100644 --- a/tests/integration/scenarios/in_http/tests/test_http2_header_limits.py +++ b/tests/integration/scenarios/in_http/tests/test_http2_header_limits.py @@ -192,3 +192,18 @@ def test_http1_health_with_http2_enabled(listener): response.read() finally: connection.close() + + +def test_http2_invalid_regular_header_resets_stream(listener): + service, tls = listener + client = Client(service.flb_listener_port, tls) + try: + headers = list(BASE_HEADERS) + headers.append((b"x-invalid", b"bad\rvalue")) + client.headers(1, headers) + result = client.result(1) + assert result.type == 3 + client.headers(3, BASE_HEADERS) + assert client.result(3).type == 1 + finally: + client.socket.close() From 7e5516929f9d20bff8fc42c797d99fa89a140f4c Mon Sep 17 00:00:00 2001 From: Hiroshi Hatake Date: Fri, 16 Oct 2026 18:50:27 +0900 Subject: [PATCH 5/6] http_server: wait for HTTP/2 end stream before dispatch Signed-off-by: Hiroshi Hatake --- src/http_server/flb_http_server_http2.c | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/src/http_server/flb_http_server_http2.c b/src/http_server/flb_http_server_http2.c index fb2ce38709b..4401810b5b9 100644 --- a/src/http_server/flb_http_server_http2.c +++ b/src/http_server/flb_http_server_http2.c @@ -881,7 +881,6 @@ static int http2_data_chunk_recv_callback(nghttp2_session *inner_session, size_t len, void *user_data) { - struct flb_http_server_session *parent_session; cfl_sds_t resized_buffer; struct flb_http_stream *stream; @@ -935,25 +934,6 @@ static int http2_data_chunk_recv_callback(nghttp2_session *inner_session, stream->request.body = resized_buffer; } - if (stream->status == HTTP_STREAM_STATUS_RECEIVING_DATA) { - if (stream->request.content_length == cfl_sds_len(stream->request.body)) { - stream->status = HTTP_STREAM_STATUS_READY; - - if (!cfl_list_entry_is_orphan(&stream->request._head)) { - cfl_list_del(&stream->request._head); - } - - parent_session = (struct flb_http_server_session *) stream->parent; - - if (parent_session == NULL) { - return -1; - } - - cfl_list_add(&stream->request._head, - &parent_session->request_queue); - } - } - return 0; } From 31a622e66fedcfa7c36c1be8ba4c7957c798df46 Mon Sep 17 00:00:00 2001 From: Hiroshi Hatake Date: Fri, 16 Oct 2026 18:50:27 +0900 Subject: [PATCH 6/6] tests: integration: cover trailers after complete DATA Signed-off-by: Hiroshi Hatake --- .../in_http/tests/test_http2_header_limits.py | 39 +++++++++++++++++-- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/tests/integration/scenarios/in_http/tests/test_http2_header_limits.py b/tests/integration/scenarios/in_http/tests/test_http2_header_limits.py index d7a364b49d3..d1d3f0b471b 100644 --- a/tests/integration/scenarios/in_http/tests/test_http2_header_limits.py +++ b/tests/integration/scenarios/in_http/tests/test_http2_header_limits.py @@ -7,7 +7,7 @@ import pytest from hpack import Decoder, Encoder -from hyperframe.frame import Frame, HeadersFrame, ContinuationFrame, SettingsFrame +from hyperframe.frame import Frame, HeadersFrame, ContinuationFrame, DataFrame, SettingsFrame from utils.test_service import FluentBitTestService @@ -44,8 +44,14 @@ def __init__(self, port, tls): self.socket.sendall(b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n" + SettingsFrame().serialize()) def headers(self, stream, headers, end=True): + frames = self.header_frames(stream, headers, end=end) + self.socket.sendall(frames) + return len(frames) + + def header_frames(self, stream, headers, end=True): # Bypass client-side advisory limits; keep valid frame sizes. block = self.encoder.encode(headers, huffman=False) + frames = bytearray() for offset in range(0, len(block), 16384): frame = HeadersFrame(stream) if offset == 0 else ContinuationFrame(stream) frame.data = block[offset:offset + 16384] @@ -53,8 +59,8 @@ def headers(self, stream, headers, end=True): frame.flags.add("END_HEADERS") if offset == 0 and end: frame.flags.add("END_STREAM") - self.socket.sendall(frame.serialize()) - return len(block) + frames.extend(frame.serialize()) + return bytes(frames) def result(self, stream): while True: @@ -207,3 +213,30 @@ def test_http2_invalid_regular_header_resets_stream(listener): assert client.result(3).type == 1 finally: client.socket.close() + + +@pytest.mark.parametrize("budget", ["count", "bytes"]) +def test_http2_trailers_after_complete_data(listener, budget): + service, tls = listener + client = Client(service.flb_listener_port, tls) + headers = list(BASE_HEADERS) + [(b"content-length", b"2")] + if budget == "count": + trailers = [(b"x-repeat", b"a")] * (MAX_FIELDS - len(headers) + 1) + else: + initial_size = sum(len(name) + len(value) + 32 for name, value in headers) + trailers = [(b"x-pad", b"a" * (MAX_BYTES - initial_size - 36))] + data = DataFrame(1) + data.data = b"{}" + try: + client.socket.sendall( + client.header_frames(1, headers, end=False) + + data.serialize() + + client.header_frames(1, trailers) + ) + result = client.result(1) + assert result.type == 3 + assert result.error_code == 11 + client.headers(3, BASE_HEADERS) + assert client.result(3).type == 1 + finally: + client.socket.close()