changeset 542:e7e3ced702f5

Tests: unbuffered request body.
author Sergey Kandaurov <pluknet@nginx.com>
date Mon, 06 Apr 2015 18:02:30 +0300
parents 53d0d963eb40
children a36757f66c75
files fastcgi_request_buffering.t fastcgi_request_buffering_chunked.t proxy_request_buffering.t proxy_request_buffering_chunked.t
diffstat 4 files changed, 1379 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
new file mode 100644
--- /dev/null
+++ b/fastcgi_request_buffering.t
@@ -0,0 +1,364 @@
+#!/usr/bin/perl
+
+# (C) Maxim Dounin
+# (C) Sergey Kandaurov
+# (C) Nginx, Inc.
+
+# Tests for unbuffered request body with fastcgi backend.
+
+###############################################################################
+
+use warnings;
+use strict;
+
+use Test::More;
+use Socket qw/ CRLF /;
+
+BEGIN { use FindBin; chdir($FindBin::Bin); }
+
+use lib 'lib';
+use Test::Nginx;
+
+###############################################################################
+
+select STDERR; $| = 1;
+select STDOUT; $| = 1;
+
+eval { require FCGI; };
+plan(skip_all => 'FCGI not installed') if $@;
+plan(skip_all => 'win32') if $^O eq 'MSWin32';
+
+my $t = Test::Nginx->new()->has(qw/http fastcgi rewrite/);
+
+$t->write_file_expand('nginx.conf', <<'EOF');
+
+%%TEST_GLOBALS%%
+
+daemon off;
+
+events {
+}
+
+http {
+    %%TEST_GLOBALS_HTTP%%
+
+    server {
+        listen       127.0.0.1:8080;
+        server_name  localhost;
+
+        client_header_buffer_size 1k;
+        fastcgi_request_buffering off;
+        fastcgi_param REQUEST_URI $request_uri;
+
+        location / {
+            client_body_buffer_size 2k;
+            add_header X-Body "$request_body";
+            fastcgi_pass 127.0.0.1:8081;
+        }
+        location /single {
+            client_body_in_single_buffer on;
+            add_header X-Body "$request_body";
+            fastcgi_pass 127.0.0.1:8081;
+        }
+        location /preread {
+            fastcgi_pass 127.0.0.1:8082;
+        }
+        location /error_page {
+            fastcgi_pass 127.0.0.1:8081;
+            error_page 404 /404;
+            fastcgi_intercept_errors on;
+        }
+        location /404 {
+            return 200 "$request_body\n";
+        }
+    }
+}
+
+EOF
+
+$t->run_daemon(\&fastcgi_daemon);
+$t->try_run('no fastcgi_request_buffering')->plan(15);
+
+$t->waitforsocket('127.0.0.1:8081');
+
+###############################################################################
+
+unlike(http_get('/'), qr/X-Body:/ms, 'no body');
+
+like(http_get_body('/', '0123456789'),
+	qr/X-Body: 0123456789\x0d?$/ms, 'body');
+
+like(http_get_body('/', '0123456789' x 128),
+	qr/X-Body: (0123456789){128}\x0d?$/ms, 'body in two buffers');
+
+like(http_get_body('/single', '0123456789' x 128),
+	qr/X-Body: (0123456789){128}\x0d?$/ms, 'body in single buffer');
+
+like(http_get_body('/error_page', '0123456789'),
+	qr/^0123456789$/m, 'body in error page');
+
+# pipelined requests
+
+like(http_get_body('/', '0123456789', '0123456789' x 128, '0123456789' x 512,
+	'foobar'), qr/X-Body: foobar\x0d?$/ms, 'body pipelined');
+like(http_get_body('/', '0123456789' x 128, '0123456789' x 512, '0123456789',
+	'foobar'), qr/X-Body: foobar\x0d?$/ms, 'body pipelined 2');
+
+# interactive tests
+
+my $s = get_body('/preread', 8082, 10);
+ok($s, 'no preread');
+
+SKIP: {
+skip 'no preread failed', 3 unless $s;
+
+is($s->{upload}('01234'), '01234', 'no preread - body part');
+is($s->{upload}('56789'), '56789', 'no preread - body part 2');
+
+like($s->{http_end}(), qr/200 OK/, 'no preread - response');
+
+}
+
+$s = get_body('/preread', 8082, 10, '01234');
+ok($s, 'preread');
+
+SKIP: {
+skip 'preread failed', 3 unless $s;
+
+is($s->{preread}, '01234', 'preread - preread');
+is($s->{upload}('56789'), '56789', 'preread - body');
+
+like($s->{http_end}(), qr/200 OK/, 'preread - response');
+
+}
+
+###############################################################################
+
+sub http_get_body {
+	my $uri = shift;
+	my $last = pop;
+	return http( join '', (map {
+		my $body = $_;
+		"GET $uri HTTP/1.1" . CRLF
+		. "Host: localhost" . CRLF
+		. "Content-Length: " . (length $body) . CRLF . CRLF
+		. $body
+	} @_),
+		"GET $uri HTTP/1.1" . CRLF
+		. "Host: localhost" . CRLF
+		. "Connection: close" . CRLF
+		. "Content-Length: " . (length $last) . CRLF . CRLF
+		. $last
+	);
+}
+
+# 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), 8));
+	$socket->write($body);
+	select(undef, undef, undef, 0.1);
+	$socket->write(pack("xxxxxxxx"));
+	select(undef, undef, undef, 0.1);
+
+	# write some text to stdout and stderr split over multiple network
+	# packets to test if we correctly set pipe length in various places
+
+	my $tt = "test text, just for test";
+
+	$socket->write(pack("CCnnCx", $version, 6, $id,
+		length($tt . $tt), 0) . $tt);
+	select(undef, undef, undef, 0.1);
+	$socket->write($tt . pack("CC", $version, 7));
+	select(undef, undef, undef, 0.1);
+	$socket->write(pack("nnCx", $id, length($tt), 0));
+	select(undef, undef, undef, 0.1);
+	$socket->write($tt);
+	select(undef, undef, undef, 0.1);
+
+	# close stdout
+	$socket->write(pack("CCnnCx", $version, 6, $id, 0, 0));
+
+	select(undef, undef, undef, 0.1);
+
+	# end request
+	$socket->write(pack("CCnnCx", $version, 3, $id, 8, 0));
+	select(undef, undef, undef, 0.1);
+	$socket->write(pack("NCxxx", 0, 0));
+}
+
+sub get_body {
+	my ($url, $port, $length, $body) = @_;
+	my ($server, $client, $s);
+	my ($version, $id);
+
+	$server = IO::Socket::INET->new(
+		Proto => 'tcp',
+		LocalHost => '127.0.0.1',
+		LocalPort => $port,
+		Listen => 5,
+		Reuse => 1
+	)
+		or die "Can't create listening socket: $!\n";
+
+	my $r = <<EOF;
+GET $url HTTP/1.1
+Host: localhost
+Connection: close
+Content-Length: $length
+
+EOF
+
+	if (defined $body) {
+		$r .= $body;
+	}
+
+	$s = http($r, start => 1);
+
+	eval {
+		local $SIG{ALRM} = sub { die "timeout\n" };
+		local $SIG{PIPE} = sub { die "sigpipe\n" };
+		alarm(5);
+
+		$client = $server->accept();
+
+		alarm(0);
+	};
+	alarm(0);
+	if ($@) {
+		log_in("died: $@");
+		return undef;
+	}
+
+	$client->sysread(my $buf, 1024);
+	$body = '';
+
+	while (my $h = fastcgi_read_record(\$buf)) {
+		$version = $h->{version};
+		$id = $h->{id};
+
+		# skip everything unless stdin
+		next if $h->{type} != 5;
+
+		$body .= $h->{content};
+	}
+
+	my $f = { preread => $body };
+	$f->{upload} = sub {
+		my $buf = shift;
+
+		eval {
+			local $SIG{ALRM} = sub { die "timeout\n" };
+			local $SIG{PIPE} = sub { die "sigpipe\n" };
+			alarm(5);
+
+			$s->write($buf);
+			$client->sysread($buf, 1024);
+			$body = '';
+
+			while (my $h = fastcgi_read_record(\$buf)) {
+
+				# skip everything unless stdin
+				next if $h->{type} != 5;
+
+				$body .= $h->{content};
+			}
+
+			alarm(0);
+		};
+		alarm(0);
+		if ($@) {
+			log_in("died: $@");
+			return undef;
+		}
+
+		return $body;
+	};
+	$f->{http_end} = sub {
+		my $buf = '';
+
+		fastcgi_respond($client, $version, $id, <<EOF);
+Status: 200 OK
+Connection: close
+X-Port: $port
+
+OK
+EOF
+
+		$client->close;
+
+		eval {
+			local $SIG{ALRM} = sub { die "timeout\n" };
+			local $SIG{PIPE} = sub { die "sigpipe\n" };
+			alarm(5);
+
+			$s->sysread($buf, 1024);
+
+			alarm(0);
+		};
+		alarm(0);
+		if ($@) {
+			log_in("died: $@");
+			return undef;
+		}
+
+		return $buf;
+	};
+	return $f;
+}
+
+###############################################################################
+
+sub fastcgi_daemon {
+	my $socket = FCGI::OpenSocket('127.0.0.1:8081', 5);
+	my $request = FCGI::Request(\*STDIN, \*STDOUT, \*STDERR, \%ENV,
+		$socket);
+
+	my $count;
+	while( $request->Accept() >= 0 ) {
+		$count++;
+
+		if ($ENV{REQUEST_URI} eq '/stderr') {
+			warn "sample stderr text" x 512;
+		}
+
+		if ($ENV{REQUEST_URI} eq '/error_page') {
+			print "Status: 404 Not Found" . CRLF . CRLF;
+			next;
+		}
+
+		print <<EOF;
+Location: http://127.0.0.1:8080/redirect
+Content-Type: text/html
+
+SEE-THIS
+$count
+EOF
+	}
+
+	FCGI::CloseSocket($socket);
+}
+
+###############################################################################
new file mode 100644
--- /dev/null
+++ b/fastcgi_request_buffering_chunked.t
@@ -0,0 +1,405 @@
+#!/usr/bin/perl
+
+# (C) Maxim Dounin
+# (C) Sergey Kandaurov
+# (C) Nginx, Inc.
+
+# Tests for unbuffered request body with fastcgi backend,
+# chunked transfer-encoding.
+
+###############################################################################
+
+use warnings;
+use strict;
+
+use Test::More;
+use Socket qw/ CRLF /;
+
+BEGIN { use FindBin; chdir($FindBin::Bin); }
+
+use lib 'lib';
+use Test::Nginx;
+
+###############################################################################
+
+select STDERR; $| = 1;
+select STDOUT; $| = 1;
+
+eval { require FCGI; };
+plan(skip_all => 'FCGI not installed') if $@;
+plan(skip_all => 'win32') if $^O eq 'MSWin32';
+
+my $t = Test::Nginx->new()->has(qw/http fastcgi rewrite/);
+
+$t->write_file_expand('nginx.conf', <<'EOF');
+
+%%TEST_GLOBALS%%
+
+daemon off;
+
+events {
+}
+
+http {
+    %%TEST_GLOBALS_HTTP%%
+
+    server {
+        listen       127.0.0.1:8080;
+        server_name  localhost;
+
+        client_header_buffer_size 1k;
+        fastcgi_request_buffering off;
+        fastcgi_param REQUEST_URI $request_uri;
+
+        location / {
+            client_body_buffer_size 2k;
+            add_header X-Body "$request_body";
+            fastcgi_pass 127.0.0.1:8081;
+        }
+        location /single {
+            client_body_in_single_buffer on;
+            add_header X-Body "$request_body";
+            fastcgi_pass 127.0.0.1:8081;
+        }
+        location /preread {
+            fastcgi_pass 127.0.0.1:8082;
+        }
+        location /error_page {
+            fastcgi_pass 127.0.0.1:8081;
+            error_page 404 /404;
+            fastcgi_intercept_errors on;
+        }
+        location /404 {
+            return 200 "$request_body\n";
+        }
+    }
+}
+
+EOF
+
+$t->run_daemon(\&fastcgi_daemon);
+$t->try_run('no fastcgi_request_buffering')->plan(19);
+
+$t->waitforsocket('127.0.0.1:8081');
+
+###############################################################################
+
+unlike(http_get('/'), qr/X-Body:/ms, 'no body');
+
+like(http_get_body('/', '0123456789'),
+	qr/X-Body: 0123456789\x0d?$/ms, 'body');
+
+like(http_get_body('/', '0123456789' x 128),
+	qr/X-Body: (0123456789){128}\x0d?$/ms, 'body in two buffers');
+
+like(http_get_body('/single', '0123456789' x 128),
+	qr/X-Body: (0123456789){128}\x0d?$/ms, 'body in single buffer');
+
+like(http_get_body('/error_page', '0123456789'),
+	qr/^0123456789$/m, 'body in error page');
+
+# pipelined requests
+
+like(http_get_body('/', '0123456789', '0123456789' x 128, '0123456789' x 512,
+	'foobar'), qr/X-Body: foobar\x0d?$/ms, 'body pipelined');
+like(http_get_body('/', '0123456789' x 128, '0123456789' x 512, '0123456789',
+	'foobar'), qr/X-Body: foobar\x0d?$/ms, 'body pipelined 2');
+
+# interactive tests
+
+my $s = get_body('/preread', 8082);
+ok($s, 'no preread');
+
+SKIP: {
+skip 'no preread failed', 3 unless $s;
+
+is($s->{upload}('01234'), '01234', 'no preread - body part');
+is($s->{upload}('56789', last => 1), '56789', 'no preread - body part 2');
+
+like($s->{http_end}(), qr/200 OK/, 'no preread - response');
+
+}
+
+$s = get_body('/preread', 8082, '01234');
+ok($s, 'preread');
+
+SKIP: {
+skip 'preread failed', 3 unless $s;
+
+is($s->{preread}, '01234', 'preread - preread');
+is($s->{upload}('56789', last => 1), '56789', 'preread - body');
+
+like($s->{http_end}(), qr/200 OK/, 'preread - response');
+
+}
+
+$s = get_body('/preread', 8082, '01234', many => 1);
+ok($s, 'chunks');
+
+SKIP: {
+skip 'chunks failed', 3 unless $s;
+
+is($s->{preread}, '01234many', 'chunks - preread');
+is($s->{upload}('56789', many => 1, last => 1), '56789many', 'chunks - body');
+
+like($s->{http_end}(), qr/200 OK/, 'chunks - response');
+
+}
+
+###############################################################################
+
+sub http_get_body {
+	my $uri = shift;
+	my $last = pop;
+	return http( join '', (map {
+		my $body = $_;
+		"GET $uri HTTP/1.1" . CRLF
+		. "Host: localhost" . CRLF
+		. "Content-Length: " . (length $body) . CRLF . CRLF
+		. $body
+	} @_),
+		"GET $uri HTTP/1.1" . CRLF
+		. "Host: localhost" . CRLF
+		. "Connection: close" . CRLF
+		. "Content-Length: " . (length $last) . CRLF . CRLF
+		. $last
+	);
+}
+
+# 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), 8));
+	$socket->write($body);
+	select(undef, undef, undef, 0.1);
+	$socket->write(pack("xxxxxxxx"));
+	select(undef, undef, undef, 0.1);
+
+	# write some text to stdout and stderr split over multiple network
+	# packets to test if we correctly set pipe length in various places
+
+	my $tt = "test text, just for test";
+
+	$socket->write(pack("CCnnCx", $version, 6, $id,
+		length($tt . $tt), 0) . $tt);
+	select(undef, undef, undef, 0.1);
+	$socket->write($tt . pack("CC", $version, 7));
+	select(undef, undef, undef, 0.1);
+	$socket->write(pack("nnCx", $id, length($tt), 0));
+	select(undef, undef, undef, 0.1);
+	$socket->write($tt);
+	select(undef, undef, undef, 0.1);
+
+	# close stdout
+	$socket->write(pack("CCnnCx", $version, 6, $id, 0, 0));
+
+	select(undef, undef, undef, 0.1);
+
+	# end request
+	$socket->write(pack("CCnnCx", $version, 3, $id, 8, 0));
+	select(undef, undef, undef, 0.1);
+	$socket->write(pack("NCxxx", 0, 0));
+}
+
+sub get_body {
+	my ($url, $port, $body, %extra) = @_;
+	my ($server, $client, $s);
+	my ($last, $many) = (0, 0);
+	my ($version, $id);
+
+	$last = $extra{last} if defined $extra{last};
+	$many = $extra{many} if defined $extra{many};
+
+	$server = IO::Socket::INET->new(
+		Proto => 'tcp',
+		LocalHost => '127.0.0.1',
+		LocalPort => $port,
+		Listen => 5,
+		Reuse => 1
+	)
+		or die "Can't create listening socket: $!\n";
+
+	my $r = <<EOF;
+GET $url HTTP/1.1
+Host: localhost
+Connection: close
+Transfer-Encoding: chunked
+
+EOF
+
+	if (defined $body) {
+		$r .= sprintf("%x", length $body) . CRLF;
+		$r .= $body . CRLF;
+	}
+	if (defined $body && $many) {
+		$r .= sprintf("%x", length 'many') . CRLF;
+		$r .= 'many' . CRLF;
+	}
+	if ($last) {
+		$r .= "0" . CRLF . CRLF;
+	}
+
+	$s = http($r, start => 1);
+
+	eval {
+		local $SIG{ALRM} = sub { die "timeout\n" };
+		local $SIG{PIPE} = sub { die "sigpipe\n" };
+		alarm(5);
+
+		$client = $server->accept();
+
+		alarm(0);
+	};
+	alarm(0);
+	if ($@) {
+		log_in("died: $@");
+		return undef;
+	}
+
+	$client->sysread(my $buf, 1024);
+	$body = '';
+
+	while (my $h = fastcgi_read_record(\$buf)) {
+		$version = $h->{version};
+		$id = $h->{id};
+
+		# skip everything unless stdin
+		next if $h->{type} != 5;
+
+		$body .= $h->{content};
+	}
+
+	my $f = { preread => $body };
+	$f->{upload} = sub {
+		my ($body, %extra) = @_;
+		my ($last, $many) = (0, 0);
+
+		$last = $extra{last} if defined $extra{last};
+		$many = $extra{many} if defined $extra{many};
+
+		my $buf = sprintf("%x", length $body) . CRLF;
+		$buf .= $body . CRLF;
+		if ($many) {
+			$buf .= sprintf("%x", length 'many') . CRLF;
+			$buf .= 'many' . CRLF;
+		}
+		if ($last) {
+			$buf .= "0" . CRLF . CRLF;
+		}
+
+		eval {
+			local $SIG{ALRM} = sub { die "timeout\n" };
+			local $SIG{PIPE} = sub { die "sigpipe\n" };
+			alarm(5);
+
+			$s->write($buf);
+			$client->sysread($buf, 1024);
+			$body = '';
+
+			while (my $h = fastcgi_read_record(\$buf)) {
+
+				# skip everything unless stdin
+				next if $h->{type} != 5;
+
+				$body .= $h->{content};
+			}
+
+			alarm(0);
+		};
+		alarm(0);
+		if ($@) {
+			log_in("died: $@");
+			return undef;
+		}
+
+		return $body;
+	};
+	$f->{http_end} = sub {
+		my $buf = '';
+
+		fastcgi_respond($client, $version, $id, <<EOF);
+Status: 200 OK
+Connection: close
+X-Port: $port
+
+OK
+EOF
+
+		$client->close;
+
+		eval {
+			local $SIG{ALRM} = sub { die "timeout\n" };
+			local $SIG{PIPE} = sub { die "sigpipe\n" };
+			alarm(5);
+
+			$s->sysread($buf, 1024);
+			$s->close();
+
+			alarm(0);
+		};
+		alarm(0);
+		if ($@) {
+			log_in("died: $@");
+			return undef;
+		}
+
+		return $buf;
+	};
+	return $f;
+}
+
+###############################################################################
+
+sub fastcgi_daemon {
+	my $socket = FCGI::OpenSocket('127.0.0.1:8081', 5);
+	my $request = FCGI::Request(\*STDIN, \*STDOUT, \*STDERR, \%ENV,
+		$socket);
+
+	my $count;
+	while( $request->Accept() >= 0 ) {
+		$count++;
+
+		if ($ENV{REQUEST_URI} eq '/stderr') {
+			warn "sample stderr text" x 512;
+		}
+
+		if ($ENV{REQUEST_URI} eq '/error_page') {
+			print "Status: 404 Not Found" . CRLF . CRLF;
+			next;
+		}
+
+		print <<EOF;
+Location: http://127.0.0.1:8080/redirect
+Content-Type: text/html
+
+SEE-THIS
+$count
+EOF
+	}
+
+	FCGI::CloseSocket($socket);
+}
+
+###############################################################################
new file mode 100644
--- /dev/null
+++ b/proxy_request_buffering.t
@@ -0,0 +1,276 @@
+#!/usr/bin/perl
+
+# (C) Maxim Dounin
+# (C) Sergey Kandaurov
+# (C) Nginx, Inc.
+
+# Tests for 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;
+
+###############################################################################
+
+select STDERR; $| = 1;
+select STDOUT; $| = 1;
+
+my $t = Test::Nginx->new()->has(qw/http 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;
+        server_name  localhost;
+
+        client_header_buffer_size 1k;
+        proxy_request_buffering off;
+
+        location / {
+            client_body_buffer_size 2k;
+            add_header X-Body "$request_body";
+            proxy_pass http://127.0.0.1:8081;
+        }
+        location /small {
+            client_body_in_file_only on;
+            proxy_pass http://127.0.0.1:8080/;
+        }
+        location /single {
+            client_body_in_single_buffer on;
+            add_header X-Body "$request_body";
+            proxy_pass http://127.0.0.1:8081;
+        }
+        location /discard {
+            return 200 "TEST\n";
+        }
+        location /preread {
+            proxy_pass http://127.0.0.1:8082/;
+        }
+        location /error_page {
+            proxy_pass http://127.0.0.1:8081/404;
+            error_page 404 /404;
+            proxy_intercept_errors on;
+        }
+        location /404 {
+            return 200 "$request_body\n";
+        }
+    }
+
+    server {
+        listen       127.0.0.1:8081;
+        server_name  localhost;
+
+        location / {
+            return 204;
+        }
+        location /404 { }
+    }
+}
+
+EOF
+
+$t->try_run('no proxy_request_buffering')->plan(18);
+
+###############################################################################
+
+unlike(http_get('/'), qr/X-Body:/ms, 'no body');
+
+like(http_get_body('/', '0123456789'),
+	qr/X-Body: 0123456789\x0d?$/ms, 'body');
+
+like(http_get_body('/', '0123456789' x 128),
+	qr/X-Body: (0123456789){128}\x0d?$/ms, 'body in two buffers');
+
+like(http_get_body('/single', '0123456789' x 128),
+	qr/X-Body: (0123456789){128}\x0d?$/ms, 'body in single buffer');
+
+like(http_get_body('/error_page', '0123456789'),
+	qr/^0123456789$/m, 'body in error page');
+
+# pipelined requests
+
+like(http_get_body('/', '0123456789', '0123456789' x 128, '0123456789' x 512,
+	'foobar'), qr/X-Body: foobar\x0d?$/ms, 'body pipelined');
+like(http_get_body('/', '0123456789' x 128, '0123456789' x 512, '0123456789',
+	'foobar'), qr/X-Body: foobar\x0d?$/ms, 'body pipelined 2');
+
+like(http_get_body('/discard', '0123456789', '0123456789' x 128,
+	'0123456789' x 512, 'foobar'), qr/(TEST.*){4}/ms,
+	'body discard');
+like(http_get_body('/discard', '0123456789' x 128, '0123456789' x 512,
+	'0123456789', 'foobar'), qr/(TEST.*){4}/ms,
+	'body discard 2');
+
+# proxy with file only is disabled in unbuffered mode
+
+like(http_get_body('/small', '0123456789'),
+	qr/X-Body: 0123456789\x0d?$/ms, 'small body in file only');
+
+# interactive tests
+
+my $s = get_body('/preread', 8082, 10);
+ok($s, 'no preread');
+
+SKIP: {
+skip 'no preread failed', 3 unless $s;
+
+is($s->{upload}('01234'), '01234', 'no preread - body part');
+is($s->{upload}('56789'), '56789', 'no preread - body part 2');
+
+like($s->{http_end}(), qr/200 OK/, 'no preread - response');
+
+}
+
+$s = get_body('/preread', 8082, 10, '01234');
+ok($s, 'preread');
+
+SKIP: {
+skip 'preread failed', 3 unless $s;
+
+is($s->{preread}, '01234', 'preread - preread');
+is($s->{upload}('56789'), '56789', 'preread - body');
+
+like($s->{http_end}(), qr/200 OK/, 'preread - response');
+
+}
+
+###############################################################################
+
+sub http_get_body {
+	my $uri = shift;
+	my $last = pop;
+	return http( join '', (map {
+		my $body = $_;
+		"GET $uri HTTP/1.1" . CRLF
+		. "Host: localhost" . CRLF
+		. "Content-Length: " . (length $body) . CRLF . CRLF
+		. $body
+	} @_),
+		"GET $uri HTTP/1.1" . CRLF
+		. "Host: localhost" . CRLF
+		. "Connection: close" . CRLF
+		. "Content-Length: " . (length $last) . CRLF . CRLF
+		. $last
+	);
+}
+
+sub get_body {
+	my ($url, $port, $length, $body) = @_;
+	my ($server, $client, $s);
+
+	$server = IO::Socket::INET->new(
+		Proto => 'tcp',
+		LocalHost => '127.0.0.1',
+		LocalPort => $port,
+		Listen => 5,
+		Reuse => 1
+	)
+		or die "Can't create listening socket: $!\n";
+
+	my $r = <<EOF;
+GET $url HTTP/1.1
+Host: localhost
+Connection: close
+Content-Length: $length
+
+EOF
+
+	if (defined $body) {
+		$r .= $body;
+	}
+
+	$s = http($r, start => 1);
+
+	eval {
+		local $SIG{ALRM} = sub { die "timeout\n" };
+		local $SIG{PIPE} = sub { die "sigpipe\n" };
+		alarm(5);
+
+		$client = $server->accept();
+
+		alarm(0);
+	};
+	alarm(0);
+	if ($@) {
+		log_in("died: $@");
+		return undef;
+	}
+
+	$client->sysread(my $buf, 1024);
+	$buf =~ s/.*?\x0d\x0a?\x0d\x0a?(.*)/$1/ms;
+
+	my $f = { preread => $buf };
+	$f->{upload} = sub {
+		my $buf = shift;
+
+		eval {
+			local $SIG{ALRM} = sub { die "timeout\n" };
+			local $SIG{PIPE} = sub { die "sigpipe\n" };
+			alarm(5);
+
+			$s->write($buf);
+			$client->sysread($buf, 1024);
+
+			alarm(0);
+		};
+		alarm(0);
+		if ($@) {
+			log_in("died: $@");
+			return undef;
+		}
+
+		return $buf;
+	};
+	$f->{http_end} = sub {
+		my $buf = '';
+
+		$client->write(<<EOF);
+HTTP/1.1 200 OK
+Connection: close
+X-Port: $port
+
+OK
+EOF
+
+		$client->close;
+
+		eval {
+			local $SIG{ALRM} = sub { die "timeout\n" };
+			local $SIG{PIPE} = sub { die "sigpipe\n" };
+			alarm(5);
+
+			$s->sysread($buf, 1024);
+
+			alarm(0);
+		};
+		alarm(0);
+		if ($@) {
+			log_in("died: $@");
+			return undef;
+		}
+
+		return $buf;
+	};
+	return $f;
+}
+
+###############################################################################
new file mode 100644
--- /dev/null
+++ b/proxy_request_buffering_chunked.t
@@ -0,0 +1,334 @@
+#!/usr/bin/perl
+
+# (C) Maxim Dounin
+# (C) Sergey Kandaurov
+# (C) Nginx, Inc.
+
+# Tests for unbuffered request body, chunked transfer-encoding.
+
+###############################################################################
+
+use warnings;
+use strict;
+
+use Test::More;
+use Socket qw/ CRLF /;
+
+BEGIN { use FindBin; chdir($FindBin::Bin); }
+
+use lib 'lib';
+use Test::Nginx;
+
+###############################################################################
+
+select STDERR; $| = 1;
+select STDOUT; $| = 1;
+
+my $t = Test::Nginx->new()->has(qw/http 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;
+        server_name  localhost;
+
+        client_header_buffer_size 1k;
+        proxy_request_buffering off;
+        proxy_http_version 1.1;
+
+        location / {
+            client_body_buffer_size 2k;
+            add_header X-Body "$request_body";
+            proxy_pass http://127.0.0.1:8081;
+        }
+        location /small {
+            client_body_in_file_only on;
+            proxy_pass http://127.0.0.1:8080/;
+        }
+        location /single {
+            client_body_in_single_buffer on;
+            add_header X-Body "$request_body";
+            proxy_pass http://127.0.0.1:8081;
+        }
+        location /discard {
+            return 200 "TEST\n";
+        }
+        location /preread {
+            proxy_pass http://127.0.0.1:8082/;
+        }
+        location /error_page {
+            proxy_pass http://127.0.0.1:8081/404;
+            error_page 404 /404;
+            proxy_intercept_errors on;
+        }
+        location /404 {
+            return 200 "$request_body\n";
+        }
+    }
+
+    server {
+        listen       127.0.0.1:8081;
+        server_name  localhost;
+
+        location / {
+            return 204;
+        }
+        location /404 { }
+    }
+}
+
+EOF
+
+$t->try_run('no proxy_request_buffering')->plan(22);
+
+###############################################################################
+
+unlike(http_get('/'), qr/X-Body:/ms, 'no body');
+
+like(http_get_body('/', '0123456789'),
+	qr/X-Body: 0123456789\x0d?$/ms, 'body');
+
+like(http_get_body('/', '0123456789' x 128),
+	qr/X-Body: (0123456789){128}\x0d?$/ms, 'body in two buffers');
+
+like(http_get_body('/single', '0123456789' x 128),
+	qr/X-Body: (0123456789){128}\x0d?$/ms, 'body in single buffer');
+
+like(http_get_body('/error_page', '0123456789'),
+	qr/^0123456789$/m, 'body in error page');
+
+# pipelined requests
+
+like(http_get_body('/', '0123456789', '0123456789' x 128, '0123456789' x 512,
+	'foobar'), qr/X-Body: foobar\x0d?$/ms, 'body pipelined');
+like(http_get_body('/', '0123456789' x 128, '0123456789' x 512, '0123456789',
+	'foobar'), qr/X-Body: foobar\x0d?$/ms, 'body pipelined 2');
+
+like(http_get_body('/discard', '0123456789', '0123456789' x 128,
+	'0123456789' x 512, 'foobar'), qr/(TEST.*){4}/ms,
+	'body discard');
+like(http_get_body('/discard', '0123456789' x 128, '0123456789' x 512,
+	'0123456789', 'foobar'), qr/(TEST.*){4}/ms,
+	'body discard 2');
+
+# proxy with file only is disabled in unbuffered mode
+
+like(http_get_body('/small', '0123456789'),
+	qr/X-Body: 0123456789\x0d?$/ms, 'small body in file only');
+
+# interactive tests
+
+my $s = get_body('/preread', 8082);
+ok($s, 'no preread');
+
+SKIP: {
+skip 'no preread failed', 3 unless $s;
+
+is($s->{upload}('01234'), '5' . CRLF . '01234' . CRLF,
+	'no preread - body part');
+is($s->{upload}('56789', last => 1),
+	'5' . CRLF . '56789' . CRLF . '0' . CRLF . CRLF,
+	'no preread - body part 2');
+
+like($s->{http_end}(), qr/200 OK/, 'no preread - response');
+
+}
+
+$s = get_body('/preread', 8082, '01234');
+ok($s, 'preread');
+
+SKIP: {
+skip 'preread failed', 3 unless $s;
+
+TODO: {
+local $TODO = 'not yet' unless $t->has_version('1.7.12');
+
+is($s->{preread}, '5' . CRLF . '01234' . CRLF, 'preread - preread');
+
+}
+
+is($s->{upload}('56789', last => 1),
+	'5' . CRLF . '56789' . CRLF . '0' . CRLF . CRLF, 'preread - body');
+
+like($s->{http_end}(), qr/200 OK/, 'preread - response');
+
+}
+
+$s = get_body('/preread', 8082, '01234', many => 1);
+ok($s, 'chunks');
+
+SKIP: {
+skip 'chunks failed', 3 unless $s;
+
+TODO: {
+local $TODO = 'not yet' unless $t->has_version('1.7.12');
+
+is($s->{preread}, '9' . CRLF . '01234many' . CRLF, 'chunks - preread');
+
+}
+
+is($s->{upload}('56789', many => 1, last => 1),
+	'9' . CRLF . '56789many' . CRLF . '0' . CRLF . CRLF, 'chunks - body');
+
+like($s->{http_end}(), qr/200 OK/, 'chunks - response');
+
+}
+
+###############################################################################
+
+sub http_get_body {
+	my $uri = shift;
+	my $last = pop;
+	return http( join '', (map {
+		my $body = $_;
+		"GET $uri HTTP/1.1" . CRLF
+		. "Host: localhost" . CRLF
+		. "Content-Length: " . (length $body) . CRLF . CRLF
+		. $body
+	} @_),
+		"GET $uri HTTP/1.1" . CRLF
+		. "Host: localhost" . CRLF
+		. "Connection: close" . CRLF
+		. "Content-Length: " . (length $last) . CRLF . CRLF
+		. $last
+	);
+}
+
+sub get_body {
+	my ($url, $port, $body, %extra) = @_;
+	my ($server, $client, $s);
+	my ($last, $many) = (0, 0);
+
+	$last = $extra{last} if defined $extra{last};
+	$many = $extra{many} if defined $extra{many};
+
+	$server = IO::Socket::INET->new(
+		Proto => 'tcp',
+		LocalHost => '127.0.0.1',
+		LocalPort => $port,
+		Listen => 5,
+		Reuse => 1
+	)
+		or die "Can't create listening socket: $!\n";
+
+	my $r = <<EOF;
+GET $url HTTP/1.1
+Host: localhost
+Connection: close
+Transfer-Encoding: chunked
+
+EOF
+
+	if (defined $body) {
+		$r .= sprintf("%x", length $body) . CRLF;
+		$r .= $body . CRLF;
+	}
+	if (defined $body && $many) {
+		$r .= sprintf("%x", length 'many') . CRLF;
+		$r .= 'many' . CRLF;
+	}
+	if ($last) {
+		$r .= "0" . CRLF . CRLF;
+	}
+
+	$s = http($r, start => 1);
+
+	eval {
+		local $SIG{ALRM} = sub { die "timeout\n" };
+		local $SIG{PIPE} = sub { die "sigpipe\n" };
+		alarm(5);
+
+		$client = $server->accept();
+
+		alarm(0);
+	};
+	alarm(0);
+	if ($@) {
+		log_in("died: $@");
+		return undef;
+	}
+
+	$client->sysread(my $buf, 1024);
+	$buf =~ s/.*?\x0d\x0a?\x0d\x0a?(.*)/$1/ms;
+
+	my $f = { preread => $buf };
+	$f->{upload} = sub {
+		my ($body, %extra) = @_;
+		my ($last, $many) = (0, 0);
+
+		$last = $extra{last} if defined $extra{last};
+		$many = $extra{many} if defined $extra{many};
+
+		my $buf = sprintf("%x", length $body) . CRLF;
+		$buf .= $body . CRLF;
+		if ($many) {
+			$buf .= sprintf("%x", length 'many') . CRLF;
+			$buf .= 'many' . CRLF;
+		}
+		if ($last) {
+			$buf .= "0" . CRLF . CRLF;
+		}
+
+		eval {
+			local $SIG{ALRM} = sub { die "timeout\n" };
+			local $SIG{PIPE} = sub { die "sigpipe\n" };
+			alarm(5);
+
+			$s->write($buf);
+			$client->sysread($buf, 1024);
+
+			alarm(0);
+		};
+		alarm(0);
+		if ($@) {
+			log_in("died: $@");
+			return undef;
+		}
+
+		return $buf;
+	};
+	$f->{http_end} = sub {
+		my $buf = '';
+
+		$client->write(<<EOF);
+HTTP/1.1 200 OK
+Connection: close
+X-Port: $port
+
+OK
+EOF
+
+		$client->close;
+
+		eval {
+			local $SIG{ALRM} = sub { die "timeout\n" };
+			local $SIG{PIPE} = sub { die "sigpipe\n" };
+			alarm(5);
+
+			$s->sysread($buf, 1024);
+			$s->close();
+
+			alarm(0);
+		};
+		alarm(0);
+		if ($@) {
+			log_in("died: $@");
+			return undef;
+		}
+
+		return $buf;
+	};
+	return $f;
+}
+
+###############################################################################