changeset 250:0c9f15938545

Tests: upstream keepalive tests imported.
author Maxim Dounin <mdounin@mdounin.ru>
date Thu, 24 Jan 2013 02:00:03 +0400
parents 6a0d934950bc
children df984d23f9d1
files fastcgi_keepalive.t memcached_keepalive.t memcached_keepalive_stale.t proxy_keepalive.t
diffstat 4 files changed, 853 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
new file mode 100644
--- /dev/null
+++ b/fastcgi_keepalive.t
@@ -0,0 +1,184 @@
+#!/usr/bin/perl
+
+# (C) Maxim Dounin
+
+# Test for fastcgi backend with keepalive.
+
+###############################################################################
+
+use warnings;
+use strict;
+
+use Test::More;
+
+BEGIN { use FindBin; chdir($FindBin::Bin); }
+
+use lib 'lib';
+use Test::Nginx;
+
+###############################################################################
+
+select STDERR; $| = 1;
+select STDOUT; $| = 1;
+
+my $t = Test::Nginx->new()->write_file_expand('nginx.conf', <<'EOF');
+
+%%TEST_GLOBALS%%
+
+daemon off;
+
+events {
+}
+
+http {
+    %%TEST_GLOBALS_HTTP%%
+
+    upstream backend {
+        server 127.0.0.1:8081;
+        keepalive 1;
+    }
+
+    server {
+        listen       127.0.0.1:8080;
+        server_name  localhost;
+
+        location / {
+            fastcgi_pass backend;
+            fastcgi_keep_conn on;
+        }
+    }
+}
+
+EOF
+
+$t->run_daemon(\&fastcgi_test_daemon);
+
+eval {
+	open OLDERR, ">&", \*STDERR; close STDERR;
+	$t->run();
+	open STDERR, ">&", \*OLDERR;
+};
+plan(skip_all => 'no keepalive patches') if $@;
+
+$t->plan(6);
+
+###############################################################################
+
+like(http_get('/'), qr/SEE-THIS/, 'fastcgi request');
+like(http_get('/redir'), qr/302/, 'fastcgi redirect');
+like(http_get('/'), qr/^request: 3$/m, 'fastcgi third request');
+
+like(http_get('/single'), qr/^connection: 1$/m, 'single connection used');
+
+# New connection to fastcgi application should be established after HEAD
+# requests since nginx doesn't read whole response (as it doesn't need
+# body).
+
+unlike(http_head('/head'), qr/SEE-THIS/, 'no data in HEAD');
+
+like(http_get('/after'), qr/^connection: 2$/m, 'new connection after HEAD');
+
+###############################################################################
+
+# Simple FastCGI responder implementation.  Unlike FCGI and FCGI::Async it's
+# able to count connections.
+
+# http://www.fastcgi.com/devkit/doc/fcgi-spec.html
+
+sub fastcgi_read_record($) {
+	my ($socket) = @_;
+
+	my ($n, $h, $header);
+
+	$n = $socket->read($header, 8);
+	return undef if !defined $n or $n != 8;
+
+	@{$h}{qw/ version type id clen plen /} = unpack("CCnnC", $header);
+
+	$n = $socket->read($h->{content}, $h->{clen});
+	return undef if $n != $h->{clen};
+
+	$n = $socket->read($h->{padding}, $h->{plen});
+	return undef if $n != $h->{plen};
+
+	$h->{socket} = $socket;
+	return $h;
+}
+
+sub fastcgi_respond($$) {
+	my ($h, $body) = @_;
+
+	# stdout
+	$h->{socket}->write(pack("CCnnCx", $h->{version}, 6, $h->{id},
+		length($body), 0));
+	$h->{socket}->write($body);
+
+	# write some text to stdout and stderr splitted over multiple network
+	# packets to test if we correctly set pipe length in various places
+
+	my $tt = "test text, just for test";
+
+	$h->{socket}->write(pack("CCnnCx", $h->{version}, 6, $h->{id},
+		length($tt . $tt), 0) . $tt);
+	select(undef, undef, undef, 0.1);
+	$h->{socket}->write($tt . pack("CC", $h->{version}, 7));
+	select(undef, undef, undef, 0.1);
+	$h->{socket}->write(pack("nnCx", $h->{id}, length($tt), 0));
+	$h->{socket}->write($tt);
+
+	# close stdout
+	$h->{socket}->write(pack("CCnnCx", $h->{version}, 6, $h->{id}, 0, 0));
+
+	select(undef, undef, undef, 0.1);
+
+	# end request
+	$h->{socket}->write(pack("CCnnCx", $h->{version}, 3, $h->{id}, 8, 0));
+	$h->{socket}->write(pack("NCxxx", 0, 0));
+}
+
+sub fastcgi_test_daemon {
+	my $server = IO::Socket::INET->new(
+		Proto => 'tcp',
+		LocalAddr => '127.0.0.1:8081',
+		Listen => 5,
+		Reuse => 1
+	)
+		or die "Can't create listening socket: $!\n";
+
+	local $SIG{PIPE} = 'IGNORE';
+
+	my $ccount = 0;
+	my $rcount = 0;
+
+	while (my $client = $server->accept()) {
+		$client->autoflush(1);
+		Test::Nginx::log_core('||', "fastcgi connection");
+
+		$ccount++;
+
+		while (my $h = fastcgi_read_record($client)) {
+			Test::Nginx::log_core('||', "fastcgi record: "
+				. " $h->{version}, $h->{type}, $h->{id}, "
+				. "'$h->{content}'");
+
+			# skip everything unless stdin, then respond
+			next if $h->{type} != 5;
+
+			$rcount++;
+
+			# respond
+			fastcgi_respond($h, <<EOF);
+Location: http://localhost:8080/redirect
+Content-Type: text/html
+
+SEE-THIS
+request: $rcount
+connection: $ccount
+EOF
+		}
+
+		close $client;
+	}
+}
+
+###############################################################################
new file mode 100644
--- /dev/null
+++ b/memcached_keepalive.t
@@ -0,0 +1,188 @@
+#!/usr/bin/perl
+
+# (C) Maxim Dounin
+
+# Test for memcached with keepalive.
+
+###############################################################################
+
+use warnings;
+use strict;
+
+use Test::More;
+
+BEGIN { use FindBin; chdir($FindBin::Bin); }
+
+use lib 'lib';
+use Test::Nginx;
+
+###############################################################################
+
+select STDERR; $| = 1;
+select STDOUT; $| = 1;
+
+eval { require Cache::Memcached; };
+plan(skip_all => 'Cache::Memcached not installed') if $@;
+
+my $t = Test::Nginx->new()->has('rewrite')->has_daemon('memcached')->plan(16)
+	->write_file_expand('nginx.conf', <<'EOF');
+
+%%TEST_GLOBALS%%
+
+daemon off;
+
+events {
+}
+
+http {
+    %%TEST_GLOBALS_HTTP%%
+
+    upstream memd {
+        server 127.0.0.1:8081;
+        keepalive 1;
+    }
+
+    upstream memd3 {
+        server 127.0.0.1:8081;
+        server 127.0.0.1:8082;
+        keepalive 1;
+    }
+
+    upstream memd4 {
+        server 127.0.0.1:8081;
+        server 127.0.0.1:8082;
+        keepalive 10;
+    }
+
+    server {
+        listen       127.0.0.1:8080;
+        server_name  localhost;
+
+        location / {
+            set $memcached_key $uri;
+            memcached_pass memd;
+        }
+
+        location /next {
+            set $memcached_key $uri;
+            memcached_next_upstream  not_found;
+            memcached_pass memd;
+        }
+
+        location /memd3 {
+            set $memcached_key "/";
+            memcached_pass memd3;
+        }
+
+        location /memd4 {
+            set $memcached_key "/";
+            memcached_pass memd4;
+        }
+    }
+}
+
+EOF
+
+my $memhelp = `memcached -h`;
+my @memopts1 = ();
+my @memopts2 = ();
+
+if ($memhelp =~ /repcached/) {
+	# repcached patches adds additional listen socket memcached
+	# that should be different too
+
+	push @memopts1, '-X', '8091';
+	push @memopts2, '-X', '8092';
+}
+if ($memhelp =~ /-U/) {
+	# UDP ports no longer off by default in memcached 1.2.7+
+
+	push @memopts1, '-U', '0';
+	push @memopts2, '-U', '0';
+}
+
+$t->run_daemon('memcached', '-l', '127.0.0.1', '-p', '8081', @memopts1);
+$t->run_daemon('memcached', '-l', '127.0.0.1', '-p', '8082', @memopts2);
+
+$t->run();
+
+$t->waitforsocket('127.0.0.1:8081')
+	or die "Unable to start memcached";
+$t->waitforsocket('127.0.0.1:8082')
+	or die "Unable to start second memcached";
+
+###############################################################################
+
+my $memd1 = Cache::Memcached->new(servers => [ '127.0.0.1:8081' ]);
+my $memd2 = Cache::Memcached->new(servers => [ '127.0.0.1:8082' ]);
+
+$memd1->set('/', 'SEE-THIS');
+$memd2->set('/', 'SEE-THIS');
+$memd1->set('/big', 'X' x 1000000);
+
+my $total = $memd1->stats()->{total}->{total_connections};
+
+like(http_get('/'), qr/SEE-THIS/, 'keepalive memcached request');
+like(http_get('/notfound'), qr/404/, 'keepalive memcached not found');
+like(http_get('/next'), qr/404/,
+	'keepalive not found with memcached_next_upstream');
+like(http_get('/'), qr/SEE-THIS/, 'keepalive memcached request again');
+like(http_get('/'), qr/SEE-THIS/, 'keepalive memcached request again');
+like(http_get('/'), qr/SEE-THIS/, 'keepalive memcached request again');
+
+is($memd1->stats()->{total}->{total_connections}, $total + 1,
+	'only one connection used');
+
+# Since nginx doesn't read all data from connection in some situations (head
+# requests, post_action, errors writing to client) we have to close such
+# connections.  Check if we really do close them.
+
+$total = $memd1->stats()->{total}->{total_connections};
+
+unlike(http_head('/'), qr/SEE-THIS/, 'head request');
+like(http_get('/'), qr/SEE-THIS/, 'get after head');
+
+is($memd1->stats()->{total}->{total_connections}, $total + 1,
+	'head request closes connection');
+
+$total = $memd1->stats()->{total}->{total_connections};
+
+unlike(http_head('/big'), qr/XXX/, 'big head');
+like(http_get('/'), qr/SEE-THIS/, 'get after big head');
+
+is($memd1->stats()->{total}->{total_connections}, $total + 1,
+	'big head request closes connection');
+
+# two backends with maximum number of cached connections set to 1,
+# should establish new connection on each request
+
+$total = $memd1->stats()->{total}->{total_connections} +
+	$memd2->stats()->{total}->{total_connections};
+
+http_get('/memd3');
+http_get('/memd3');
+http_get('/memd3');
+
+is($memd1->stats()->{total}->{total_connections} +
+	$memd2->stats()->{total}->{total_connections}, $total + 3,
+	'3 connections should be established');
+
+# two backends with maximum number of cached connections set to 10,
+# should establish only two connections (1 per backend)
+
+$total = $memd1->stats()->{total}->{total_connections} +
+	$memd2->stats()->{total}->{total_connections};
+
+http_get('/memd4');
+http_get('/memd4');
+http_get('/memd4');
+
+is($memd1->stats()->{total}->{total_connections} +
+	$memd2->stats()->{total}->{total_connections}, $total + 2,
+	'connection per backend');
+
+$t->stop();
+
+like(`grep -F '[alert]' ${\($t->testdir())}/error.log`, qr/^$/s, 'no alerts');
+
+###############################################################################
new file mode 100644
--- /dev/null
+++ b/memcached_keepalive_stale.t
@@ -0,0 +1,118 @@
+#!/usr/bin/perl
+
+# (C) Maxim Dounin
+
+# Test for stale events handling in upstream keepalive.
+
+###############################################################################
+
+use warnings;
+use strict;
+
+use Test::More;
+
+BEGIN { use FindBin; chdir($FindBin::Bin); }
+
+use lib 'lib';
+use Test::Nginx;
+
+###############################################################################
+
+select STDERR; $| = 1;
+select STDOUT; $| = 1;
+
+eval { require Cache::Memcached; };
+plan(skip_all => 'Cache::Memcached not installed') if $@;
+
+my $t = Test::Nginx->new()->has('rewrite')->has_daemon('memcached')->plan(2)
+	->write_file_expand('nginx.conf', <<'EOF');
+
+%%TEST_GLOBALS%%
+
+daemon off;
+
+worker_processes 2;
+
+events {
+}
+
+http {
+    %%TEST_GLOBALS_HTTP%%
+
+    upstream memd {
+        server 127.0.0.1:8081;
+        keepalive 1;
+    }
+
+    server {
+        listen       127.0.0.1:8080 sndbuf=32k;
+        server_name  localhost;
+
+        location / {
+            set $memcached_key $uri;
+            memcached_pass memd;
+        }
+    }
+}
+
+EOF
+
+my $memhelp = `memcached -h`;
+my @memopts1 = ();
+
+if ($memhelp =~ /repcached/) {
+	# repcached patches adds additional listen socket memcached
+	# that should be different too
+
+	push @memopts1, '-X', '8091';
+}
+if ($memhelp =~ /-U/) {
+	# UDP ports no longer off by default in memcached 1.2.7+
+
+	push @memopts1, '-U', '0';
+}
+
+$t->run_daemon('memcached', '-l', '127.0.0.1', '-p', '8081', @memopts1);
+
+$t->run();
+
+$t->waitforsocket('127.0.0.1:8081')
+	or die "Unable to start memcached";
+
+###############################################################################
+
+my $memd1 = Cache::Memcached->new(servers => [ '127.0.0.1:8081' ]);
+
+# It's possible that stale events occur, i.e. read event handler called
+# for just saved upstream connection without any data available for
+# read.  We shouldn't close upstream connection in such situation.
+#
+# This happens due to reading from upstream connection on downstream write
+# events.  More likely to happen with multiple workers due to use of posted
+# events.
+#
+# Stale event may only happen if reading response from upstream requires
+# entering event loop, i.e. response should be big enough.  On the other
+# hand, it is less likely to occur with full client's connection output
+# buffer.
+#
+# We use here 2 workers, 20k response and set output buffer on clients
+# connection to 32k.  This allows more or less reliably reproduce stale
+# events at least on FreeBSD testbed here.
+
+$memd1->set('/big', 'X' x 20480);
+
+my $total = $memd1->stats()->{total}->{total_connections};
+
+for (1 .. 100) {
+	http_get('/big');
+}
+
+cmp_ok($memd1->stats()->{total}->{total_connections}, '<=', $total + 2,
+	'only one connection per worker used');
+
+$t->stop();
+
+like(`grep -F '[alert]' ${\($t->testdir())}/error.log`, qr/^$/s, 'no alerts');
+
+###############################################################################
new file mode 100644
--- /dev/null
+++ b/proxy_keepalive.t
@@ -0,0 +1,363 @@
+#!/usr/bin/perl
+
+# (C) Maxim Dounin
+
+# Tests for proxy with keepalive.
+
+###############################################################################
+
+use warnings;
+use strict;
+
+use Test::More;
+
+use IO::Socket::INET;
+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 ssi rewrite/)
+	->write_file_expand('nginx.conf', <<'EOF');
+
+%%TEST_GLOBALS%%
+
+daemon off;
+
+events {
+}
+
+http {
+    %%TEST_GLOBALS_HTTP%%
+
+    upstream backend {
+        server 127.0.0.1:8081;
+        keepalive 1;
+    }
+
+    server {
+        listen       127.0.0.1:8080;
+        server_name  localhost;
+
+        proxy_read_timeout 2s;
+        proxy_http_version 1.1;
+        proxy_set_header Connection "";
+
+        location / {
+            proxy_pass http://backend;
+        }
+
+        location /unbuffered/ {
+            proxy_pass http://backend;
+            proxy_buffering off;
+        }
+
+        location /inmemory/ {
+            ssi on;
+            rewrite ^ /ssi.html break;
+        }
+    }
+}
+
+EOF
+
+$t->write_file('ssi.html',
+	'<!--#include virtual="/include$request_uri" set="x" -->' .
+	'set: <!--#echo var="x" -->');
+
+$t->run_daemon(\&http_daemon);
+
+eval {
+	open OLDERR, ">&", \*STDERR; close STDERR;
+	$t->run();
+	open STDERR, ">&", \*OLDERR;
+};
+plan(skip_all => 'no keepalive patches') if $@;
+
+$t->plan(50);
+
+###############################################################################
+
+# There are 3 mostly independend modes of upstream operation:
+#
+# 1. Buffered, i.e. normal mode with "proxy_buffering on;"
+# 2. Unbuffered, i.e. "proxy_buffering off;".
+# 3. In memory, i.e. ssi <!--#include ... set -->
+#
+# These all should be tested.
+
+my ($r, $n);
+
+# buffered
+
+like($r = http_get('/buffered/length1'), qr/SEE-THIS/, 'buffered');
+$r =~ m/X-Connection: (\d+)/; $n = $1;
+like(http_get('/buffered/length2'), qr/X-Connection: $n.*SEE/ms, 'buffered 2');
+
+like($r = http_get('/buffered/chunked1'), qr/SEE-THIS/, 'buffered chunked');
+$r =~ m/X-Connection: (\d+)/; $n = $1;
+like(http_get('/buffered/chunked2'), qr/X-Connection: $n/,
+	'buffered chunked 2');
+
+like($r = http_get('/buffered/complex1'), qr/(0123456789){100}/,
+	'buffered complex chunked');
+$r =~ m/X-Connection: (\d+)/; $n = $1;
+like(http_get('/buffered/complex2'), qr/X-Connection: $n/,
+	'buffered complex chunked 2');
+
+like($r = http_get('/buffered/chunk01'), qr/200 OK/, 'buffered 0 chunk');
+$r =~ m/X-Connection: (\d+)/; $n = $1;
+like(http_get('/buffered/chunk02'), qr/X-Connection: $n/, 'buffered 0 chunk 2');
+
+like($r = http_head('/buffered/length/head1'), qr/(?!SEE-THIS)/,
+	'buffered head');
+$r =~ m/X-Connection: (\d+)/; $n = $1;
+like(http_head('/buffered/length/head2'), qr/X-Connection: $n/,
+	'buffered head 2');
+
+like($r = http_get('/buffered/empty1'), qr/200 OK/, 'buffered empty');
+$r =~ m/X-Connection: (\d+)/; $n = $1;
+like(http_get('/buffered/empty2'), qr/X-Connection: $n/, 'buffered empty 2');
+
+like($r = http_get('/buffered/304nolen1'), qr/304 Not/, 'buffered 304');
+$r =~ m/X-Connection: (\d+)/; $n = $1;
+like(http_get('/buffered/304nolen2'), qr/X-Connection: $n/, 'buffered 304 2');
+
+like($r = http_get('/buffered/304len1'), qr/304 Not/,
+	'buffered 304 with length');
+$r =~ m/X-Connection: (\d+)/; $n = $1;
+like(http_get('/buffered/304len2'), qr/X-Connection: $n/,
+	'buffered 304 with length 2');
+
+# unbuffered
+
+like($r = http_get('/unbuffered/length1'), qr/SEE-THIS/, 'unbuffered');
+$r =~ m/X-Connection: (\d+)/; $n = $1;
+like(http_get('/unbuffered/length2'), qr/X-Connection: $n/, 'unbuffered 2');
+
+like($r = http_get('/unbuffered/chunked1'), qr/SEE-THIS/, 'unbuffered chunked');
+$r =~ m/X-Connection: (\d+)/; $n = $1;
+like(http_get('/unbuffered/chunked2'), qr/X-Connection: $n/,
+	'unbuffered chunked 2');
+
+like($r = http_get('/unbuffered/complex1'), qr/(0123456789){100}/,
+	'unbuffered complex chunked');
+$r =~ m/X-Connection: (\d+)/; $n = $1;
+like(http_get('/unbuffered/complex2'), qr/X-Connection: $n/,
+	'unbuffered complex chunked 2');
+
+like($r = http_get('/unbuffered/chunk01'), qr/200 OK/, 'unbuffered 0 chunk');
+$r =~ m/X-Connection: (\d+)/; $n = $1;
+like(http_get('/unbuffered/chunk02'), qr/X-Connection: $n/,
+	'unbuffered 0 chunk 2');
+
+like($r = http_get('/unbuffered/empty1'), qr/200 OK/, 'unbuffered empty');
+$r =~ m/X-Connection: (\d+)/; $n = $1;
+like(http_get('/unbuffered/empty2'), qr/X-Connection: $n/,
+	'unbuffered empty 2');
+
+like($r = http_head('/unbuffered/length/head1'), qr/(?!SEE-THIS)/,
+	'unbuffered head');
+$r =~ m/X-Connection: (\d+)/; $n = $1;
+like(http_head('/unbuffered/length/head2'), qr/X-Connection: $n/,
+	'unbuffered head 2');
+
+like($r = http_get('/unbuffered/304nolen1'), qr/304 Not/, 'unbuffered 304');
+$r =~ m/X-Connection: (\d+)/; $n = $1;
+like(http_get('/unbuffered/304nolen2'), qr/X-Connection: $n/,
+	'unbuffered 304 2');
+
+like($r = http_get('/unbuffered/304len1'), qr/304 Not/,
+	'unbuffered 304 with length');
+$r =~ m/X-Connection: (\d+)/; $n = $1;
+like(http_get('/unbuffered/304len2'), qr/X-Connection: $n/,
+	'unbuffered 304 with length 2');
+
+# in memory
+
+like($r = http_get('/inmemory/length1'), qr/SEE-THIS/, 'inmemory');
+$r =~ m/SEE-THIS(\d+)/; $n = $1;
+like(http_get('/inmemory/length2'), qr/SEE-THIS$n/, 'inmemory 2');
+
+like($r = http_get('/inmemory/empty1'), qr/200 OK/, 'inmemory empty');
+$r =~ m/SEE-THIS(\d+)/; $n = $1;
+like(http_get('/inmemory/empty2'), qr/200 OK/, 'inmemory empty 2');
+
+like($r = http_get('/inmemory/chunked1'), qr/SEE-THIS/, 'inmemory chunked');
+$r =~ m/SEE-THIS(\d+)/; $n = $1;
+like(http_get('/inmemory/chunked2'), qr/SEE-THIS$n/, 'inmemory chunked 2');
+
+like($r = http_get('/inmemory/complex1'), qr/(0123456789){100}/,
+	'inmemory complex chunked');
+$r =~ m/SEE-THIS(\d+)/; $n = $1;
+like(http_get('/inmemory/complex2'), qr/SEE-THIS$n/,
+	'inmemory complex chunked 2');
+
+like(http_get('/inmemory/chunk01'), qr/set: $/, 'inmemory 0 chunk');
+like(http_get('/inmemory/chunk02'), qr/set: $/, 'inmemory 0 chunk 2');
+
+# closed connection tests
+
+like(http_get('/buffered/closed1'), qr/200 OK/, 'buffered closed 1');
+like(http_get('/buffered/closed2'), qr/200 OK/, 'buffered closed 2');
+like(http_get('/unbuffered/closed1'), qr/200 OK/, 'unbuffered closed 1');
+like(http_get('/unbuffered/closed2'), qr/200 OK/, 'unbuffered closed 2');
+like(http_get('/inmemory/closed1'), qr/200 OK/, 'inmemory closed 1');
+like(http_get('/inmemory/closed2'), qr/200 OK/, 'inmemory closed 2');
+
+# check for errors, shouldn't be any
+
+like(`grep -F '[alert]' ${\($t->testdir())}/error.log`, qr/^$/s, 'no alerts');
+like(`grep -F '[error]' ${\($t->testdir())}/error.log`, qr/^$/s, 'no errors');
+
+###############################################################################
+
+sub http_daemon {
+	my $server = IO::Socket::INET->new(
+		Proto => 'tcp',
+		LocalHost => '127.0.0.1:8081',
+		Listen => 5,
+		Reuse => 1
+	)
+		or die "Can't create listening socket: $!\n";
+
+	my $ccount = 0;
+	my $rcount = 0;
+
+	# dumb server which is able to keep connections alive
+
+	while (my $client = $server->accept()) {
+		Test::Nginx::log_core('||',
+			"connection from " . $client->peerhost());
+		$client->autoflush(1);
+		$ccount++;
+
+		while (1) {
+			my $headers = '';
+			my $uri = '';
+
+			while (<$client>) {
+				Test::Nginx::log_core('||', $_);
+				$headers .= $_;
+				last if (/^\x0d?\x0a?$/);
+			}
+
+			last if $headers eq '';
+			$rcount++;
+
+			$uri = $1 if $headers =~ /^\S+\s+([^ ]+)\s+HTTP/i;
+
+			if ($uri =~ m/length/) {
+				print $client
+					"HTTP/1.1 200 OK" . CRLF .
+					"X-Request: $rcount" . CRLF .
+					"X-Connection: $ccount" . CRLF .
+					"Content-Length: 26" . CRLF . CRLF;
+				print $client "TEST-OK-IF-YOU-SEE-THIS" .
+					sprintf("%03d", $ccount)
+					unless $headers =~ /^HEAD/i;
+
+			} elsif ($uri =~ m/empty/) {
+				print $client
+					"HTTP/1.1 200 OK" . CRLF .
+					"X-Request: $rcount" . CRLF .
+					"X-Connection: $ccount" . CRLF .
+					"Content-Length: 0" . CRLF . CRLF;
+
+			} elsif ($uri =~ m/304nolen/) {
+				print $client
+					"HTTP/1.1 304 Not Modified" . CRLF .
+					"X-Request: $rcount" . CRLF .
+					"X-Connection: $ccount" . CRLF . CRLF;
+
+			} elsif ($uri =~ m/304len/) {
+				print $client
+					"HTTP/1.1 304 Not Modified" . CRLF .
+					"X-Request: $rcount" . CRLF .
+					"X-Connection: $ccount" . CRLF .
+					"Content-Length: 100" . CRLF . CRLF;
+
+			} elsif ($uri =~ m/chunked/) {
+				print $client
+					"HTTP/1.1 200 OK" . CRLF .
+					"X-Request: $rcount" . CRLF .
+					"X-Connection: $ccount" . CRLF .
+					"Transfer-Encoding: chunked" . CRLF .
+					CRLF;
+				print $client
+					"1a" . CRLF .
+					"TEST-OK-IF-YOU-SEE-THIS" .
+					sprintf("%03d", $ccount) . CRLF .
+					"0" . CRLF . CRLF
+					unless $headers =~ /^HEAD/i;
+
+			} elsif ($uri =~ m/complex/) {
+				print $client
+					"HTTP/1.1 200 OK" . CRLF .
+					"X-Request: $rcount" . CRLF .
+					"X-Connection: $ccount" . CRLF .
+					"Transfer-Encoding: chunked" . CRLF .
+					CRLF;
+
+				if ($headers !~ /^HEAD/i) {
+					for my $n (1..100) {
+						print $client
+							"a" . CRLF .
+							"0123456789" . CRLF;
+						select undef, undef, undef, 0.01
+							if $n % 50 == 0;
+					}
+					print $client
+						"1a" . CRLF .
+						"TEST-OK-IF-YOU-SEE-THIS" .
+						sprintf("%03d", $ccount) .
+						CRLF .
+						"0" . CRLF;
+					select undef, undef, undef, 0.05;
+					print $client CRLF;
+				}
+
+			} elsif ($uri =~ m/chunk0/) {
+				print $client
+					"HTTP/1.1 200 OK" . CRLF .
+					"X-Request: $rcount" . CRLF .
+					"X-Connection: $ccount" . CRLF .
+					"Transfer-Encoding: chunked" . CRLF .
+					CRLF;
+				print $client
+					"0" . CRLF . CRLF
+					unless $headers =~ /^HEAD/i;
+
+			} elsif ($uri =~ m/closed/) {
+				print $client
+					"HTTP/1.1 200 OK" . CRLF .
+					"X-Request: $rcount" . CRLF .
+					"X-Connection: $ccount" . CRLF .
+					"Connection: close" . CRLF .
+					"Content-Length: 12" . CRLF . CRLF .
+					"0123456789" . CRLF;
+				last;
+
+			} else {
+				print $client
+					"HTTP/1.1 404 Not Found" . CRLF .
+					"X-Request: $rcount" . CRLF .
+					"X-Connection: $ccount" . CRLF .
+					"Connection: close" . CRLF . CRLF .
+					"Oops, '$uri' not found" . CRLF;
+				last;
+			}
+		}
+
+		close $client;
+	}
+}
+
+###############################################################################