changeset 879:127a602f36c8

Tests: HTTP/2 tests for unbuffered request body.
author Sergey Kandaurov <pluknet@nginx.com>
date Wed, 23 Mar 2016 20:08:20 +0300
parents 327044615c87
children 697fc80b8606
files h2_fastcgi_request_buffering.t h2_proxy_request_buffering.t h2_proxy_request_buffering_ssl.t
diffstat 3 files changed, 785 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
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, <<EOF);
+Status: 200 OK
+Connection: close
+
+OK
+EOF
+
+		$client->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('||', @_); }
+
+###############################################################################
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(<<EOF);
+HTTP/1.1 200 OK
+Connection: close
+
+EOF
+
+		$client->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('||', @_); }
+
+###############################################################################
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', <<EOF);
+[ req ]
+default_bits = 2048
+encrypt_key = no
+distinguished_name = req_distinguished_name
+[ req_distinguished_name ]
+EOF
+
+my $d = $t->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(<<EOF);
+HTTP/1.1 200 OK
+Connection: close
+
+EOF
+
+		$client->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('||', @_); }
+
+###############################################################################