Tests: additional HTTP/2 request body tests.
diff --git a/h2_request_body_extra.t b/h2_request_body_extra.t
new file mode 100644
index 0000000..38d3db0
--- /dev/null
+++ b/h2_request_body_extra.t
@@ -0,0 +1,331 @@
+#!/usr/bin/perl
+
+# (C) Maxim Dounin
+# (C) Nginx, Inc.
+
+# Tests for HTTP/2 protocol with request body, additional tests.
+
+###############################################################################
+
+use warnings;
+use strict;
+
+use Test::More;
+
+BEGIN { use FindBin; chdir($FindBin::Bin); }
+
+use lib 'lib';
+use Test::Nginx;
+use Test::Nginx::HTTP2;
+
+###############################################################################
+
+select STDERR; $| = 1;
+select STDOUT; $| = 1;
+
+my $t = Test::Nginx->new()->has(qw/http http_v2 proxy rewrite/);
+
+$t->write_file_expand('nginx.conf', <<'EOF');
+
+%%TEST_GLOBALS%%
+
+daemon off;
+
+events {
+}
+
+http {
+    %%TEST_GLOBALS_HTTP%%
+
+    server {
+        listen       127.0.0.1:8080 http2;
+        listen       127.0.0.1:8081;
+        server_name  localhost;
+
+        client_header_buffer_size 1k;
+        client_body_buffer_size 2k;
+
+        location / {
+            add_header X-Body $request_body;
+            add_header X-Body-File $request_body_file;
+            proxy_pass http://127.0.0.1:8082;
+        }
+
+        location /file {
+            client_body_in_file_only on;
+            add_header X-Body "$request_body";
+            add_header X-Body-File "$request_body_file";
+            proxy_pass http://127.0.0.1:8082;
+        }
+
+        location /single {
+            client_body_in_single_buffer on;
+            add_header X-Body "$request_body";
+            add_header X-Body-File "$request_body_file";
+            proxy_pass http://127.0.0.1:8082;
+        }
+
+        location /large {
+            client_max_body_size 1k;
+            proxy_pass http://127.0.0.1:8082;
+        }
+
+        location /unbuf/ {
+            add_header X-Unbuf-File "$request_body_file";
+            proxy_pass http://127.0.0.1:8081/;
+            proxy_request_buffering off;
+            proxy_http_version 1.1;
+        }
+    }
+
+    server {
+        listen       127.0.0.1:8082;
+        server_name  localhost;
+        return 204;
+    }
+}
+
+EOF
+
+plan(skip_all => 'not yet') unless $t->has_version('1.21.2');
+$t->plan(50);
+
+$t->run();
+
+###############################################################################
+
+# below are basic body tests from body.t, slightly
+# adapted to HTTP/2, repeated multiple times with variations:
+#
+# buffered vs. non-buffered, length vs. chunked,
+# single frame vs. multiple frames
+#
+# some does not make sense in HTTP/2 (such as "body in two buffers"), but
+# preserved for consistency and due to the fact that proxying via HTTP/1.1
+# is used in unbuffered tests
+
+unlike(http2_get('/'), qr/x-body:/ms, 'no body');
+
+like(http2_get_body('/', '0123456789'),
+	qr/x-body: 0123456789$/ms, 'body');
+like(http2_get_body('/', '0123456789' x 128),
+	qr/x-body: (0123456789){128}$/ms, 'body in two buffers');
+like(http2_get_body('/', '0123456789' x 512),
+	qr/x-body-file/ms, 'body in file');
+like(read_body_file(http2_get_body('/file', '0123456789' x 512)),
+	qr/^(0123456789){512}$/s, 'body in file only');
+like(http2_get_body('/single', '0123456789' x 128),
+	qr/x-body: (0123456789){128}$/ms, 'body in single buffer');
+like(http2_get_body('/large', '0123456789' x 128),
+	qr/:status: 413/, 'body too large');
+
+# without Content-Length header
+
+like(http2_get_body_nolen('/', '0123456789'),
+	qr/x-body: 0123456789$/ms, 'body nolen');
+like(http2_get_body_nolen('/', '0123456789' x 128),
+	qr/x-body: (0123456789){128}$/ms, 'body nolen in two buffers');
+like(http2_get_body_nolen('/', '0123456789' x 512),
+	qr/x-body-file/ms, 'body nolen in file');
+like(read_body_file(http2_get_body_nolen('/file', '0123456789' x 512)),
+	qr/^(0123456789){512}$/s, 'body nolen in file only');
+like(http2_get_body_nolen('/single', '0123456789' x 128),
+	qr/x-body: (0123456789){128}$/ms, 'body nolen in single buffer');
+like(http2_get_body_nolen('/large', '0123456789' x 128),
+	qr/:status: 413/, 'body nolen too large');
+
+# with multiple frames
+
+like(http2_get_body_multi('/', '0123456789'),
+	qr/x-body: 0123456789$/ms, 'body multi');
+like(http2_get_body_multi('/', '0123456789' x 128),
+	qr/x-body: (0123456789){128}$/ms, 'body multi in two buffers');
+like(http2_get_body_multi('/', '0123456789' x 512),
+	qr/x-body-file/ms, 'body multi in file');
+like(read_body_file(http2_get_body_multi('/file', '0123456789' x 512)),
+	qr/^(0123456789){512}$/s, 'body multi in file only');
+like(http2_get_body_multi('/single', '0123456789' x 128),
+	qr/x-body: (0123456789){128}$/ms, 'body multi in single buffer');
+like(http2_get_body_multi('/large', '0123456789' x 128),
+	qr/:status: 413/, 'body multi too large');
+
+# with multiple frames and without Content-Length header
+
+like(http2_get_body_multi_nolen('/', '0123456789'),
+	qr/x-body: 0123456789$/ms, 'body multi nolen');
+like(http2_get_body_multi_nolen('/', '0123456789' x 128),
+	qr/x-body: (0123456789){128}/ms, 'body multi nolen in two buffers');
+like(http2_get_body_multi_nolen('/', '0123456789' x 512),
+	qr/x-body-file/ms, 'body multi nolen in file');
+like(read_body_file(http2_get_body_multi_nolen('/file', '0123456789' x 512)),
+	qr/^(0123456789){512}$/s, 'body multi nolen in file only');
+like(http2_get_body_multi_nolen('/single', '0123456789' x 128),
+	qr/x-body: (0123456789){128}$/ms, 'body multi nolen in single buffer');
+like(http2_get_body_multi_nolen('/large', '0123456789' x 128),
+	qr/:status: 413/, 'body multi nolen too large');
+
+# unbuffered
+
+unlike(http2_get('/unbuf/'), qr/x-body:/ms, 'no body unbuf');
+
+like(http2_get_body('/unbuf/', '0123456789'),
+	qr/x-body: 0123456789$/ms, 'body unbuf');
+like(http2_get_body('/unbuf/', '0123456789' x 128),
+	qr/x-body: (0123456789){128}$/ms, 'body unbuf in two buffers');
+like(http2_get_body('/unbuf/', '0123456789' x 512),
+	qr/(?!.*x-unbuf-file.*)x-body-file/ms, 'body unbuf in file');
+like(read_body_file(http2_get_body('/unbuf/file', '0123456789' x 512)),
+	qr/^(0123456789){512}$/s, 'body unbuf in file only');
+like(http2_get_body('/unbuf/single', '0123456789' x 128),
+	qr/x-body: (0123456789){128}$/ms, 'body unbuf in single buffer');
+like(http2_get_body('/unbuf/large', '0123456789' x 128),
+	qr/:status: 413/, 'body unbuf too large');
+
+# unbuffered without Content-Length
+
+like(http2_get_body_nolen('/unbuf/', '0123456789'),
+	qr/x-body: 0123456789$/ms, 'body unbuf nolen');
+like(http2_get_body_nolen('/unbuf/', '0123456789' x 128),
+	qr/x-body: (0123456789){128}$/ms, 'body unbuf nolen in two buffers');
+like(http2_get_body_nolen('/unbuf/', '0123456789' x 512),
+	qr/(?!.*x-unbuf-file.*)x-body-file/ms, 'body unbuf nolen in file');
+like(read_body_file(http2_get_body_nolen('/unbuf/file', '0123456789' x 512)),
+	qr/^(0123456789){512}$/s, 'body unbuf nolen in file only');
+like(http2_get_body_nolen('/unbuf/single', '0123456789' x 128),
+	qr/x-body: (0123456789){128}$/ms, 'body unbuf nolen in single buffer');
+like(http2_get_body_nolen('/unbuf/large', '0123456789' x 128),
+	qr/:status: 413/, 'body unbuf nolen too large');
+
+# unbuffered with multiple frames
+
+like(http2_get_body_multi('/unbuf/', '0123456789'),
+	qr/x-body: 0123456789$/ms, 'body unbuf multi');
+like(http2_get_body_multi('/unbuf/', '0123456789' x 128),
+	qr/x-body: (0123456789){128}$/ms, 'body unbuf multi in two buffers');
+like(http2_get_body_multi('/unbuf/', '0123456789' x 512),
+	qr/(?!.*x-unbuf-file.*)x-body-file/ms, 'body unbuf multi in file');
+like(read_body_file(http2_get_body_multi('/unbuf/file', '0123456789' x 512)),
+	qr/^(0123456789){512}$/s, 'body unbuf multi in file only');
+like(http2_get_body_multi('/unbuf/single', '0123456789' x 128),
+	qr/x-body: (0123456789){128}$/ms, 'body unbuf multi in single buffer');
+like(http2_get_body_multi('/unbuf/large', '0123456789' x 128),
+	qr/:status: 413/, 'body unbuf multi too large');
+
+# unbuffered with multiple frames and without Content-Length
+
+like(http2_get_body_multi_nolen('/unbuf/', '0123456789'),
+	qr/x-body: 0123456789$/ms, 'body unbuf multi nolen');
+like(http2_get_body_multi_nolen('/unbuf/', '0123456789' x 128),
+	qr/x-body: (0123456789){128}$/ms,
+	'body unbuf multi nolen in two buffers');
+like(http2_get_body_multi_nolen('/unbuf/', '0123456789' x 512),
+	qr/(?!.*x-unbuf-file.*)x-body-file/ms,
+        'body unbuf multi nolen in file');
+like(read_body_file(http2_get_body_multi_nolen('/unbuf/file',
+	'0123456789' x 512)), qr/^(0123456789){512}$/s,
+	'body unbuf multi nolen in file only');
+like(http2_get_body_multi_nolen('/unbuf/single', '0123456789' x 128),
+	qr/x-body: (0123456789){128}$/ms,
+	'body unbuf multi nolen in single buffer');
+like(http2_get_body_multi_nolen('/unbuf/large', '0123456789' x 128),
+	qr/:status: 413/, 'body unbuf multi nolen too large');
+
+###############################################################################
+
+sub http2_get {
+	my ($uri) = @_;
+
+	my $s = Test::Nginx::HTTP2->new();
+	my $sid = $s->new_stream({ path => $uri });
+	my $frames = $s->read(all => [{ sid => $sid, fin => 1 }]);
+
+	my ($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
+
+	return join("\n", map { "$_: " . $frame->{headers}->{$_}; }
+		keys %{$frame->{headers}});
+}
+
+sub http2_get_body {
+	my ($uri, $body) = @_;
+
+	my $s = Test::Nginx::HTTP2->new();
+	my $sid = $s->new_stream({ path => $uri, body => $body });
+	my $frames = $s->read(all => [{ sid => $sid, fin => 1 }]);
+
+	my ($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
+
+	return join("\n", map { "$_: " . $frame->{headers}->{$_}; }
+			keys %{$frame->{headers}});
+}
+
+sub http2_get_body_nolen {
+	my ($uri, $body) = @_;
+
+	my $s = Test::Nginx::HTTP2->new();
+	my $sid = $s->new_stream({ path => $uri, body_more => 1 });
+	$s->h2_body($body);
+	my $frames = $s->read(all => [{ sid => $sid, fin => 1 }]);
+
+	my ($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
+
+	return join("\n", map { "$_: " . $frame->{headers}->{$_}; }
+			keys %{$frame->{headers}});
+}
+
+sub http2_get_body_multi {
+	my ($uri, $body) = @_;
+
+	my $s = Test::Nginx::HTTP2->new();
+	my $sid = $s->new_stream({
+		headers => [
+			{ name => ':method', value => 'GET' },
+			{ name => ':scheme', value => 'http' },
+			{ name => ':path', value => $uri },
+			{ name => ':authority', value => 'localhost' },
+			{ name => 'content-length', value => length $body },
+		],
+		body_more => 1
+	});
+	for my $b (split //, $body, 10) {
+		$s->h2_body($b, { body_more => 1 });
+	}
+	select undef, undef, undef, 0.1;
+	$s->h2_body('');
+	my $frames = $s->read(all => [{ sid => $sid, fin => 1 }]);
+
+	my ($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
+
+	return join("\n", map { "$_: " . $frame->{headers}->{$_}; }
+			keys %{$frame->{headers}});
+}
+
+sub http2_get_body_multi_nolen {
+	my ($uri, $body) = @_;
+
+	my $s = Test::Nginx::HTTP2->new();
+	my $sid = $s->new_stream({ path => $uri, body_more => 1 });
+	for my $b (split //, $body, 10) {
+		$s->h2_body($b, { body_more => 1 });
+	}
+	select undef, undef, undef, 0.1;
+	$s->h2_body('');
+	my $frames = $s->read(all => [{ sid => $sid, fin => 1 }]);
+
+	my ($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
+
+	return join("\n", map { "$_: " . $frame->{headers}->{$_}; }
+			keys %{$frame->{headers}});
+}
+
+sub read_body_file {
+	my ($r) = @_;
+	return '' unless $r =~ m/x-body-file: (.*)/;
+	open FILE, $1
+		or return "$!";
+	local $/;
+	my $content = <FILE>;
+	close FILE;
+	return $content;
+}
+
+###############################################################################