# HG changeset patch # User Sergey Kandaurov # Date 1458752900 -10800 # Node ID 127a602f36c88b7b7c3da3552aae7fd6934ff686 # Parent 327044615c879dc689c680639f56cf191b8412f4 Tests: HTTP/2 tests for unbuffered request body. diff --git a/h2_fastcgi_request_buffering.t b/h2_fastcgi_request_buffering.t new file mode 100644 --- /dev/null +++ b/h2_fastcgi_request_buffering.t @@ -0,0 +1,274 @@ +#!/usr/bin/perl + +# (C) Sergey Kandaurov +# (C) Nginx, Inc. + +# Tests for HTTP/2 protocol with unbuffered request body and fastcgi backend. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; +use Test::Nginx::HTTP2 qw/ :DEFAULT :frame :io /; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +my $t = Test::Nginx->new()->has(qw/http http_v2 fastcgi/); + +$t->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + server { + listen 127.0.0.1:8080 http2; + server_name localhost; + + location / { + fastcgi_request_buffering off; + fastcgi_pass 127.0.0.1:8081; + fastcgi_param REQUEST_URI $request_uri; + client_body_buffer_size 1k; + } + } +} + +EOF + +$t->run(); + +plan(skip_all => 'no unbuffered request body') unless get_body('/'); + +$t->plan(48); + +############################################################################### + +my ($f); + +# unbuffered request body to fastcgi + +$f = get_body('/'); +ok($f->{headers}, 'request'); +is($f->{upload}('01234', body_more => 1), '01234', 'part'); +is($f->{upload}('56789'), '56789_eos', 'part 2'); +is($f->{http_end}(), 200, 'response'); + +$f = get_body('/'); +ok($f->{headers}, 'buffer'); +is($f->{upload}('0123' x 128, body_more => 1), '0123' x 128, 'buffer - below'); +is($f->{upload}('4567' x 128, body_more => 1), '4567' x 128, 'buffer - equal'); +is($f->{upload}('89AB' x 128), '89AB' x 128 . '_eos', 'buffer - above'); +is($f->{http_end}(), 200, 'buffer - response'); + +$f = get_body('/'); +ok($f->{headers}, 'many'); +is($f->{upload}('01234many', body_split => [ 5 ], body_more => 1), + '01234many', 'many - part'); +is($f->{upload}('56789many', body_split => [ 5 ]), + '56789many_eos', 'many - part 2'); +is($f->{http_end}(), 200, 'many - response'); + +$f = get_body('/'); +ok($f->{headers}, 'empty'); +is($f->{upload}('', body_more => 1), '', 'empty - part'); +is($f->{upload}(''), '_eos', 'empty - part 2'); +is($f->{http_end}(), 200, 'empty - response'); + +$f = get_body('/'); +ok($f->{headers}, 'split'); +is($f->{upload}('0123456789', split => [ 14 ]), '0123456789_eos', + 'split - part'); +is($f->{http_end}(), 200, 'split - response'); + +# unbuffered request body to fastcgi, content-length + +$f = get_body('/', 'content-length' => 10); +ok($f->{headers}, 'cl'); + +is($f->{upload}('01234', body_more => 1), '01234', 'cl - part'); +is($f->{upload}('56789'), '56789_eos', 'cl - part 2'); +is($f->{http_end}(), 200, 'cl - response'); + +$f = get_body('/', 'content-length' => 1536); +ok($f->{headers}, 'cl buffer'); +is($f->{upload}('0123' x 128, body_more => 1), '0123' x 128, + 'cl buffer - below'); +is($f->{upload}('4567' x 128, body_more => 1), '4567' x 128, + 'cl buffer - equal'); +is($f->{upload}('89AB' x 128), '89AB' x 128 . '_eos', 'cl buffer - above'); +is($f->{http_end}(), 200, 'cl buffer - response'); + +$f = get_body('/', 'content-length' => 10); +ok($f->{headers}, 'cl much'); +is($f->{upload}('0123456789', body_more => 1), '0123456789', 'cl much - part'); +is($f->{upload}('many'), '', 'cl much - part 2'); +is($f->{http_end}(), 400, 'cl much - response'); + +$f = get_body('/', 'content-length' => 10); +ok($f->{headers}, 'cl less'); +is($f->{upload}('0123', body_more => 1), '0123', 'cl less - part'); +is($f->{upload}('56789'), '', 'cl less - part 2'); +is($f->{http_end}(), 400, 'cl less - response'); + +$f = get_body('/', 'content-length' => 18); +ok($f->{headers}, 'cl many'); +is($f->{upload}('01234many', body_split => [ 5 ], body_more => 1), + '01234many', 'cl many - part'); +is($f->{upload}('56789many', body_split => [ 5 ]), '56789many_eos', + 'cl many - part 2'); +is($f->{http_end}(), 200, 'cl many - response'); + +$f = get_body('/', 'content-length' => 0); +ok($f->{headers}, 'cl empty'); +is($f->{upload}('', body_more => 1), '', 'cl empty - part'); +is($f->{upload}(''), '_eos', 'cl empty - part 2'); +is($f->{http_end}(), 200, 'cl empty - response'); + +$f = get_body('/', 'content-length' => 10); +ok($f->{headers}, 'cl split'); +is($f->{upload}('0123456789', split => [ 14 ]), '0123456789_eos', 'cl split'); +is($f->{http_end}(), 200, 'cl split - response'); + +############################################################################### + +# Simple FastCGI responder implementation. + +# http://www.fastcgi.com/devkit/doc/fcgi-spec.html + +sub fastcgi_read_record($) { + my ($buf) = @_; + + my ($n, $h, $header); + + return undef unless length $$buf; + + @{$h}{qw/ version type id clen plen /} = unpack("CCnnC", $$buf); + + $h->{content} = substr $$buf, 8, $h->{clen}; + $h->{padding} = substr $$buf, 8 + $h->{clen}, $h->{plen}; + + $$buf = substr $$buf, 8 + $h->{clen} + $h->{plen}; + + return $h; +} + +sub fastcgi_respond($$$$) { + my ($socket, $version, $id, $body) = @_; + + # stdout + $socket->write(pack("CCnnCx", $version, 6, $id, length($body), 0)); + $socket->write($body); + + # close stdout + $socket->write(pack("CCnnCx", $version, 6, $id, 0, 0)); + + # end request + $socket->write(pack("CCnnCx", $version, 3, $id, 8, 0)); + $socket->write(pack("NCxxx", 0, 0)); +} + +sub get_body { + my ($url, %extra) = @_; + my ($server, $client, $f); + + $server = IO::Socket::INET->new( + Proto => 'tcp', + LocalHost => '127.0.0.1', + LocalPort => 8081, + Listen => 5, + Timeout => 3, + Reuse => 1 + ) + or die "Can't create listening socket: $!\n"; + + my $sess = new_session(8080); + my $sid = exists $extra{'content-length'} + ? new_stream($sess, { headers => [ + { name => ':method', value => 'GET' }, + { name => ':scheme', value => 'http' }, + { name => ':path', value => $url, }, + { name => ':authority', value => 'localhost' }, + { name => 'content-length', + value => $extra{'content-length'} }], + body_more => 1 }) + : new_stream($sess, { path => $url, body_more => 1 }); + + $client = $server->accept() or return; + + log2c("(new connection $client)"); + + $f->{headers} = raw_read($client, '', 1, \&log2i); + + my $h = fastcgi_read_record(\$f->{headers}); + my $version = $h->{version}; + my $id = $h->{id}; + + $f->{upload} = sub { + my ($body, %extra) = @_; + my $len = length($body); + + h2_body($sess, $body, { %extra }); + + $body = ''; + + for (1 .. 10) { + my $buf = raw_read($client, '', 1, \&log2i) + or return ''; + + while (my $h = fastcgi_read_record(\$buf)) { + + # skip everything unless stdin + next if $h->{type} != 5; + + $body .= $h->{content}; + + # mark the end-of-stream indication + $body .= "_eos" if $h->{clen} == 0; + } + + last if length($body) >= $len; + } + + return $body; + }; + $f->{http_end} = sub { + local $SIG{PIPE} = 'IGNORE'; + + fastcgi_respond($client, $version, $id, <close; + + my $frames = h2_read($sess, all => [{ sid => $sid, fin => 1 }]); + my ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; + return $frame->{headers}->{':status'}; + }; + return $f; +} + +sub log2i { Test::Nginx::log_core('|| <<', @_); } +sub log2o { Test::Nginx::log_core('|| >>', @_); } +sub log2c { Test::Nginx::log_core('||', @_); } + +############################################################################### diff --git a/h2_proxy_request_buffering.t b/h2_proxy_request_buffering.t new file mode 100644 --- /dev/null +++ b/h2_proxy_request_buffering.t @@ -0,0 +1,243 @@ +#!/usr/bin/perl + +# (C) Sergey Kandaurov +# (C) Nginx, Inc. + +# Tests for HTTP/2 protocol with unbuffered request body. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +use Socket qw/ CRLF /; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; +use Test::Nginx::HTTP2 qw/ :DEFAULT :frame :io /; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +my $t = Test::Nginx->new()->has(qw/http http_v2 proxy/); + +$t->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + server { + listen 127.0.0.1:8080 http2; + server_name localhost; + + location / { + proxy_request_buffering off; + proxy_pass http://127.0.0.1:8081/; + client_body_buffer_size 1k; + } + location /chunked { + proxy_request_buffering off; + proxy_http_version 1.1; + proxy_pass http://127.0.0.1:8081/; + client_body_buffer_size 1k; + } + } +} + +EOF + +$t->run(); + +plan(skip_all => 'no unbuffered request body') unless get_body('/chunked'); + +$t->plan(48); + +############################################################################### + +my ($f); + +# unbuffered request body + +$f = get_body('/', 'content-length' => 10); +ok($f->{headers}, 'request'); +is($f->{upload}('01234', body_more => 1), '01234', 'part'); +is($f->{upload}('56789'), '56789', 'part 2'); +is($f->{http_end}(), 200, 'response'); + +$f = get_body('/', 'content-length' => 10); +ok($f->{headers}, 'much'); +is($f->{upload}('0123456789', body_more => 1), '0123456789', 'much - part'); +is($f->{upload}('many'), '', 'much - part 2'); +is($f->{http_end}(), 400, 'much - response'); + +$f = get_body('/', 'content-length' => 10); +ok($f->{headers}, 'less'); +is($f->{upload}('0123', body_more => 1), '0123', 'less - part'); +is($f->{upload}('56789'), '', 'less - part 2'); +is($f->{http_end}(), 400, 'less - response'); + +$f = get_body('/', 'content-length' => 18); +ok($f->{headers}, 'many'); +is($f->{upload}('01234many', body_split => [ 5 ], body_more => 1), + '01234many', 'many - part'); +is($f->{upload}('56789many', body_split => [ 5 ]), + '56789many', 'many - part 2'); +is($f->{http_end}(), 200, 'many - response'); + +$f = get_body('/', 'content-length' => 0); +ok($f->{headers}, 'empty'); +is($f->{upload}('', body_more => 1), '', 'empty - part'); +is($f->{upload}(''), '', 'empty - part 2'); +is($f->{http_end}(), 200, 'empty - response'); + +$f = get_body('/', 'content-length' => 1536); +ok($f->{headers}, 'buffer'); +is($f->{upload}('0123' x 128, body_more => 1), '0123' x 128, + 'buffer - below'); +is($f->{upload}('4567' x 128, body_more => 1), '4567' x 128, + 'buffer - equal'); +is($f->{upload}('89AB' x 128), '89AB' x 128, 'buffer - above'); +is($f->{http_end}(), 200, 'buffer - response'); + +$f = get_body('/', 'content-length' => 10); +ok($f->{headers}, 'split'); +is($f->{upload}('0123456789', split => [ 14 ]), '0123456789', 'split'); +is($f->{http_end}(), 200, 'split - response'); + +# unbuffered request body, chunked transfer-encoding + +$f = get_body('/chunked'); +ok($f->{headers}, 'chunked'); +is($f->{upload}('01234', body_more => 1), '5' . CRLF . '01234' . CRLF, + 'chunked - part'); +is($f->{upload}('56789'), '5' . CRLF . '56789' . CRLF . '0' . CRLF . CRLF, + 'chunked - part 2'); +is($f->{http_end}(), 200, 'chunked - response'); + +$f = get_body('/chunked'); +ok($f->{headers}, 'chunked buffer'); +is($f->{upload}('0123' x 128, body_more => 1), + '200' . CRLF . '0123' x 128 . CRLF, 'chunked buffer - below'); +is($f->{upload}('4567' x 128, body_more => 1), + '200' . CRLF . '4567' x 128 . CRLF, 'chunked buffer - equal'); +is($f->{upload}('89AB' x 128), + '200' . CRLF . '89AB' x 128 . CRLF . '0' . CRLF . CRLF, + 'chunked buffer - above'); +is($f->{http_end}(), 200, 'chunked buffer - response'); + +$f = get_body('/chunked'); +ok($f->{headers}, 'chunked many'); +is($f->{upload}('01234many', body_split => [ 5 ], body_more => 1), + '9' . CRLF . '01234many' . CRLF, 'chunked many - part'); +is($f->{upload}('56789many', body_split => [ 5 ]), + '9' . CRLF . '56789many' . CRLF . '0' . CRLF . CRLF, + 'chunked many - part 2'); +is($f->{http_end}(), 200, 'chunked many - response'); + +$f = get_body('/chunked'); +ok($f->{headers}, 'chunked empty'); +is($f->{upload}('', body_more => 1), '', 'chunked empty - part'); +is($f->{upload}(''), '0' . CRLF . CRLF, 'chunked empty - part 2'); +is($f->{http_end}(), 200, 'chunked empty - response'); + +$f = get_body('/chunked'); +ok($f->{headers}, 'chunked split'); +is($f->{upload}('0123456789', split => [ 14 ]), + '5' . CRLF . '01234' . CRLF . '5' . CRLF . '56789' . CRLF . + '0' . CRLF . CRLF, 'chunked split'); +is($f->{http_end}(), 200, 'chunked split - response'); + +############################################################################### + +sub get_body { + my ($url, %extra) = @_; + my ($server, $client, $f); + + $server = IO::Socket::INET->new( + Proto => 'tcp', + LocalHost => '127.0.0.1', + LocalPort => 8081, + Listen => 5, + Timeout => 3, + Reuse => 1 + ) + or die "Can't create listening socket: $!\n"; + + my $sess = new_session(8080); + my $sid = exists $extra{'content-length'} + ? new_stream($sess, { headers => [ + { name => ':method', value => 'GET' }, + { name => ':scheme', value => 'http' }, + { name => ':path', value => $url, }, + { name => ':authority', value => 'localhost' }, + { name => 'content-length', + value => $extra{'content-length'} }], + body_more => 1 }) + : new_stream($sess, { path => $url, body_more => 1 }); + + $client = $server->accept() or return; + + log2c("(new connection $client)"); + + $f->{headers} = raw_read($client, '', 1, \&log2i); + + my $chunked = $f->{headers} =~ /chunked/; + + my $body_read = sub { + my ($s, $buf, $len) = @_; + + for (1 .. 10) { + $buf = raw_read($s, $buf, length($buf) + 1, \&log2i) + or return ''; + + my $got = 0; + $got += $chunked ? hex $_ : $_ for $chunked + ? $buf =~ /(\w+)\x0d\x0a?\w+\x0d\x0a?/g + : length($buf); + last if $got >= $len; + } + + return $buf; + }; + + $f->{upload} = sub { + my ($body, %extra) = @_; + + h2_body($sess, $body, { %extra }); + + return $body_read->($client, '', length($body)); + }; + $f->{http_end} = sub { + $client->write(<close; + + my $frames = h2_read($sess, all => [{ sid => $sid, fin => 1 }]); + my ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; + return $frame->{headers}->{':status'}; + }; + return $f; +} + +sub log2i { Test::Nginx::log_core('|| <<', @_); } +sub log2o { Test::Nginx::log_core('|| >>', @_); } +sub log2c { Test::Nginx::log_core('||', @_); } + +############################################################################### diff --git a/h2_proxy_request_buffering_ssl.t b/h2_proxy_request_buffering_ssl.t new file mode 100644 --- /dev/null +++ b/h2_proxy_request_buffering_ssl.t @@ -0,0 +1,268 @@ +#!/usr/bin/perl + +# (C) Sergey Kandaurov +# (C) Nginx, Inc. + +# Tests for HTTP/2 protocol with unbuffered request body to ssl backend. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +use Socket qw/ CRLF /; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; +use Test::Nginx::HTTP2 qw/ :DEFAULT :frame :io /; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +my $t = Test::Nginx->new()->has(qw/http http_ssl http_v2 proxy/) + ->has_daemon('openssl'); + +$t->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + server { + listen 127.0.0.1:8080 http2; + server_name localhost; + + location / { + proxy_request_buffering off; + proxy_pass https://127.0.0.1:8082; + client_body_buffer_size 512; + } + location /chunked { + proxy_request_buffering off; + proxy_http_version 1.1; + proxy_pass https://127.0.0.1:8082; + client_body_buffer_size 512; + } + } + + server { + listen 127.0.0.1:8082 ssl; + server_name localhost; + + ssl_certificate_key localhost.key; + ssl_certificate localhost.crt; + + location / { + proxy_request_buffering off; + proxy_pass http://127.0.0.1:8081/; + client_body_buffer_size 1k; + } + location /chunked { + proxy_request_buffering off; + proxy_http_version 1.1; + proxy_pass http://127.0.0.1:8081/; + client_body_buffer_size 1k; + } + } +} + +EOF + +$t->write_file('openssl.conf', <testdir(); + +foreach my $name ('localhost') { + system('openssl req -x509 -new ' + . "-config '$d/openssl.conf' -subj '/CN=$name/' " + . "-out '$d/$name.crt' -keyout '$d/$name.key' " + . ">>$d/openssl.out 2>&1") == 0 + or die "Can't create certificate for $name: $!\n"; +} + +$t->run(); + +plan(skip_all => 'no unbuffered request body') unless get_body('/chunked'); + +$t->plan(40); + +############################################################################### + +my ($f); + +# unbuffered request body + +$f = get_body('/', 'content-length' => 10); +ok($f->{headers}, 'request'); +is($f->{upload}('01234', body_more => 1), '01234', 'part'); +is($f->{upload}('56789'), '56789', 'part 2'); +is($f->{http_end}(), 200, 'response'); + +$f = get_body('/', 'content-length' => 1536); +ok($f->{headers}, 'buffer'); +is($f->{upload}('0123' x 128, body_more => 1), '0123' x 128, 'buffer - below'); +is($f->{upload}('4567' x 128, body_more => 1), '4567' x 128, 'buffer - equal'); +is($f->{upload}('89AB' x 128), '89AB' x 128, 'buffer - above'); +is($f->{http_end}(), 200, 'buffer - response'); + +$f = get_body('/', 'content-length' => 18); +ok($f->{headers}, 'many'); +is($f->{upload}('01234many', body_split => [ 5 ], body_more => 1), + '01234many', 'many - part'); +is($f->{upload}('56789many', body_split => [ 5 ]), + '56789many', 'many - part 2'); +is($f->{http_end}(), 200, 'many - response'); + +$f = get_body('/', 'content-length' => 0); +ok($f->{headers}, 'empty'); +is($f->{upload}('', body_more => 1), '', 'empty - part'); +is($f->{upload}(''), '', 'empty - part 2'); +is($f->{http_end}(), 200, 'empty - response'); + +$f = get_body('/', 'content-length' => 10); +ok($f->{headers}, 'split'); +is($f->{upload}('0123456789', split => [ 14 ]), '0123456789', 'split'); +is($f->{http_end}(), 200, 'split - response'); + +# unbuffered request body, chunked transfer-encoding + +$f = get_body('/chunked'); +ok($f->{headers}, 'chunk'); +is($f->{upload}('01234', body_more => 1), '5' . CRLF . '01234' . CRLF, + 'chunked - part'); +is($f->{upload}('56789'), '5' . CRLF . '56789' . CRLF . '0' . CRLF . CRLF, + 'chunked - part 2'); +is($f->{http_end}(), 200, 'chunked - response'); + +$f = get_body('/chunked'); +ok($f->{headers}, 'chunked buffer'); +is($f->{upload}('0123' x 64, body_more => 1), + '100' . CRLF . '0123' x 64 . CRLF, 'chunked buffer - below'); +is($f->{upload}('4567' x 64, body_more => 1), + '100' . CRLF . '4567' x 64 . CRLF, 'chunked buffer - equal'); +is($f->{upload}('89AB' x 64), + '100' . CRLF . '89AB' x 64 . CRLF . '0' . CRLF . CRLF, + 'chunked buffer - above'); +is($f->{http_end}(), 200, 'chunked buffer - response'); + +$f = get_body('/chunked'); +ok($f->{headers}, 'chunked many'); +is($f->{upload}('01234many', body_split => [ 5 ], body_more => 1), + '9' . CRLF . '01234many' . CRLF, 'chunked many - part'); +is($f->{upload}('56789many', body_split => [ 5 ]), + '9' . CRLF . '56789many' . CRLF . '0' . CRLF . CRLF, + 'chunked many - part 2'); +is($f->{http_end}(), 200, 'chunked many - response'); + +$f = get_body('/chunked'); +ok($f->{headers}, 'chunked empty'); +is($f->{upload}('', body_more => 1), '', 'chunked empty - part'); +is($f->{upload}(''), '0' . CRLF . CRLF, 'chunked empty - part 2'); +is($f->{http_end}(), 200, 'chunked empty - response'); + +$f = get_body('/chunked'); +ok($f->{headers}, 'chunked split'); +is($f->{upload}('0123456789', split => [ 14 ]), + '5' . CRLF . '01234' . CRLF . '5' . CRLF . '56789' . CRLF . + '0' . CRLF . CRLF, 'chunked split'); +is($f->{http_end}(), 200, 'chunked split - response'); + +############################################################################### + +sub get_body { + my ($url, %extra) = @_; + my ($server, $client, $f); + + $server = IO::Socket::INET->new( + Proto => 'tcp', + LocalHost => '127.0.0.1', + LocalPort => 8081, + Listen => 5, + Timeout => 3, + Reuse => 1 + ) + or die "Can't create listening socket: $!\n"; + + my $sess = new_session(8080); + my $sid = exists $extra{'content-length'} + ? new_stream($sess, { headers => [ + { name => ':method', value => 'GET' }, + { name => ':scheme', value => 'http' }, + { name => ':path', value => $url, }, + { name => ':authority', value => 'localhost' }, + { name => 'content-length', + value => $extra{'content-length'} }], + body_more => 1 }) + : new_stream($sess, { path => $url, body_more => 1 }); + + $client = $server->accept() or return; + + log2c("(new connection $client)"); + + $f->{headers} = raw_read($client, '', 1, \&log2i); + + my $chunked = $f->{headers} =~ /chunked/; + + my $body_read = sub { + my ($s, $buf, $len) = @_; + + for (1 .. 10) { + $buf = raw_read($s, $buf, length($buf) + 1, \&log2i) + or return ''; + + my $got = 0; + $got += $chunked ? hex $_ : $_ for $chunked + ? $buf =~ /(\w+)\x0d\x0a?\w+\x0d\x0a?/g + : length($buf); + last if $got >= $len; + } + + return $buf; + }; + + $f->{upload} = sub { + my ($body, %extra) = @_; + + h2_body($sess, $body, { %extra }); + + return $body_read->($client, '', length($body)); + }; + $f->{http_end} = sub { + $client->write(<close; + + my $frames = h2_read($sess, all => [{ sid => $sid, fin => 1 }]); + my ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; + return $frame->{headers}->{':status'}; + }; + return $f; +} + +sub log2i { Test::Nginx::log_core('|| <<', @_); } +sub log2o { Test::Nginx::log_core('|| >>', @_); } +sub log2c { Test::Nginx::log_core('||', @_); } + +###############################################################################