From cb4543e638e053cf6b4edf138b09de6518d5f8fc Mon Sep 17 00:00:00 2001 From: tahmid-23 <60953955+tahmid-23@users.noreply.github.com> Date: Thu, 6 Aug 2026 04:29:15 -0400 Subject: [PATCH] Tests: HTTP/3 reserved frame type on a request stream Added tests for a reserved frame (RFC 9114, Section 7.2.8) sent on a request stream whose body is read: with a payload after the body, between two DATA frames, and with an empty payload. Without the corresponding fix, the first two are rejected as a prematurely closed stream and the second splices frame header bytes into the request body. A h3_frame() helper is added to send a frame of an arbitrary type. --- h3_request_body.t | 45 ++++++++++++++++++++++++++++++++++++++++- lib/Test/Nginx/HTTP3.pm | 13 ++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/h3_request_body.t b/h3_request_body.t index 8943cef6..d2bba4dc 100644 --- a/h3_request_body.t +++ b/h3_request_body.t @@ -24,7 +24,7 @@ select STDERR; $| = 1; select STDOUT; $| = 1; my $t = Test::Nginx->new()->has(qw/http http_v3 proxy cryptx/) - ->has_daemon('openssl')->plan(30); + ->has_daemon('openssl')->plan(34); $t->write_file_expand('nginx.conf', <<'EOF'); @@ -136,6 +136,49 @@ is(read_body_file($frame->{headers}->{'x-body-file'}), 'TESTMOREDATA', is($frame->{headers}->{'x-length'}, 12, 'request body in multiple frames separately - content length'); +# reserved frame with a payload after the body + +$s = Test::Nginx::HTTP3->new(); +$sid = $s->new_stream({ path => '/proxy/t.html', body_more => 1, + body => 'TEST' }); +$s->h3_frame(0x1f * 42 + 0x21, 'grease', $sid); +$frames = $s->read(all => [{ sid => $sid, fin => 1 }]); + +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{headers}->{':status'}, 200, 'reserved frame - status'); +is(read_body_file($frame->{headers}->{'x-body-file'}), 'TEST', + 'reserved frame - body'); + +# reserved frame between two DATA frames + +$s = Test::Nginx::HTTP3->new(); +$sid = $s->new_stream({ body_more => 1, headers => [ + { name => ':method', value => 'GET' }, + { name => ':scheme', value => 'http' }, + { name => ':path', value => '/proxy/t.html' }, + { name => ':authority', value => 'localhost' }, + { name => 'content-length', value => '8' }]}); +$s->h3_body('TEST', $sid, { body_more => 1 }); +$s->h3_frame(0x1f * 42 + 0x21, 'grease', $sid, { body_more => 1 }); +$s->h3_body('MORE', $sid); +$frames = $s->read(all => [{ sid => $sid, fin => 1 }]); + +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is(read_body_file($frame->{headers}->{'x-body-file'}), 'TESTMORE', + 'reserved frame between data - body'); + +# reserved frame with an empty payload + +$s = Test::Nginx::HTTP3->new(); +$sid = $s->new_stream({ path => '/proxy/t.html', body_more => 1, + body => 'TEST' }); +$s->h3_frame(0x1f * 42 + 0x21, '', $sid); +$frames = $s->read(all => [{ sid => $sid, fin => 1 }]); + +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is(read_body_file($frame->{headers}->{'x-body-file'}), 'TEST', + 'reserved frame empty - body'); + # request body with an empty DATA frame $s = Test::Nginx::HTTP3->new(); diff --git a/lib/Test/Nginx/HTTP3.pm b/lib/Test/Nginx/HTTP3.pm index 995ae93b..bd0f4e91 100644 --- a/lib/Test/Nginx/HTTP3.pm +++ b/lib/Test/Nginx/HTTP3.pm @@ -507,6 +507,19 @@ sub pack_body { $buf .= $body; } +sub h3_frame { + my ($self, $type, $body, $sid, $extra) = @_; + + $body = '' unless defined $body; + my $buf = build_int($type) . build_int(length($body)) . $body; + + my $offset = $extra->{offset} || $self->{streams}{$sid}{sent}; + + $self->{streams}{$sid}{sent} += length($buf); + $self->raw_write($self->build_stream($buf, + start => $extra->{body_more}, sid => $sid, offset => $offset)); +} + sub h3_max_data { my ($self, $val, $stream) = @_;