# HG changeset patch # User Hiroaki Nakamura <hnaka...@gmail.com> # Date 1719322456 -32400 # Tue Jun 25 22:34:16 2024 +0900 # Node ID 758a6c4c1babb9d60d6f507ee409152eeb51cbc5 # Parent a1e4c426f1063c7a2ab30e60df7e1756ae7d8918 Tests: Update and add tests for Age header.
diff -r a1e4c426f106 -r 758a6c4c1bab h2_proxy_cache_age.t --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/h2_proxy_cache_age.t Tue Jun 25 22:34:16 2024 +0900 @@ -0,0 +1,203 @@ +#!/usr/bin/perl + +# (C) Sergey Kandaurov +# (C) Nginx, Inc. +# (C) Hiroaki Nakamura + +# Tests for age in HTTP/2 proxy cache. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; +use Test::Nginx::HTTP2; + +use POSIX qw/ ceil /; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +my $t = Test::Nginx->new()->has(qw/http http_v2 proxy cache/)->plan(8) + ->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + proxy_cache_path %%TESTDIR%%/cache keys_zone=NAME:1m; + proxy_cache_path %%TESTDIR%%/cache2 keys_zone=NAME2:1m; + + map $arg_slow $rate { + default 8k; + 1 90; + } + + server { + listen 127.0.0.1:8080 http2; + server_name localhost; + error_log %%TESTDIR%%/error8080.log debug; + + location / { + proxy_pass http://127.0.0.1:8081; + proxy_cache NAME; + proxy_http_version 1.1; + proxy_cache_revalidate on; + } + } + + server { + listen 127.0.0.1:8081; + server_name localhost; + error_log %%TESTDIR%%/error8081.log debug; + + location / { + proxy_pass http://127.0.0.1:8082; + proxy_cache NAME2; + proxy_http_version 1.1; + proxy_cache_revalidate on; + } + } + + server { + listen 127.0.0.1:8082; + server_name localhost; + error_log %%TESTDIR%%/error8082.log debug; + + location / { + add_header Cache-Control s-maxage=$arg_ttl; + limit_rate $rate; + } + } +} + +EOF + +$t->write_file('t.html', 'SEE-THIS'); + +# suppress deprecation warning + +open OLDERR, ">&", \*STDERR; close STDERR; +$t->run(); +open STDERR, ">&", \*OLDERR; + +############################################################################### + +my $s = Test::Nginx::HTTP2->new(); + +my ($path, $sid, $frames, $frame, $t1, $resident_time); + +# normal origin + +wait_until_next_second(); + +$path = '/t.html?ttl=2'; + +$sid = $s->new_stream({ path => $path }); +$frames = $s->read(all => [{ sid => $sid, fin => 1 }]); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{headers}->{'age'}, undef, 'age first'); + +select undef, undef, undef, 2.0; + +$sid = $s->new_stream({ path => $path }); +$frames = $s->read(all => [{ sid => $sid, fin => 1 }]); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{headers}->{'age'}, 2, 'age hit'); + +select undef, undef, undef, 1.0; + +$sid = $s->new_stream({ path => $path }); +$frames = $s->read(all => [{ sid => $sid, fin => 1 }]); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{headers}->{'age'}, undef, 'age updated'); + +SKIP: { +skip 'no exec on win32', 3 if $^O eq 'MSWin32'; + +# slow origin + +wait_until_next_second(); + +$path = '/t.html?ttl=4&slow=1'; + +$sid = $s->new_stream({ path => $path }); +$frames = $s->read(all => [{ sid => $sid, fin => 1 }]); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{headers}->{'age'}, 2, 'slow origin first'); + +select undef, undef, undef, 2.0; + +$sid = $s->new_stream({ path => $path }); +$frames = $s->read(all => [{ sid => $sid, fin => 1 }]); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{headers}->{'age'}, 4, 'slow origin hit'); + +select undef, undef, undef, 1.0; + +$sid = $s->new_stream({ path => $path }); +$frames = $s->read(all => [{ sid => $sid, fin => 1 }]); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{headers}->{'age'}, 2, 'slow origin updated'); + +} + +# update age after restart + +wait_until_next_second(); + +$path = '/t.html?ttl=20'; + +$sid = $s->new_stream({ path => $path }); +$frames = $s->read(all => [{ sid => $sid, fin => 1 }]); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{headers}->{'age'}, undef, 'age before restart'); +$t1 = time(); + +$t->stop(); + +select undef, undef, undef, 1.0; + +open OLDERR, ">&", \*STDERR; close STDERR; +$t->run(); +open STDERR, ">&", \*OLDERR; + +$resident_time = time() - $t1; + +$s = Test::Nginx::HTTP2->new(); + +$sid = $s->new_stream({ path => $path }); +$frames = $s->read(all => [{ sid => $sid, fin => 1 }]); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{headers}->{'age'}, $resident_time, 'age after restart'); + +$t->stop(); + +############################################################################### + +# Wait until the next second boundary. +# Calling this before sending a request increases the likelihood that the +# timestamp value does not cross into the next second while sending a request +# and receiving a response. +sub wait_until_next_second { + my $now = time(); + my $next_second = ceil($now); + my $sleep = $next_second - $now; + select undef, undef, undef, $sleep; +} + +############################################################################### diff -r a1e4c426f106 -r 758a6c4c1bab h2_ssl_proxy_cache_age.t --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/h2_ssl_proxy_cache_age.t Tue Jun 25 22:34:16 2024 +0900 @@ -0,0 +1,248 @@ +#!/usr/bin/perl + +# (C) Sergey Kandaurov +# (C) Nginx, Inc. +# (C) Hiroaki Nakamura + +# Tests for age in HTTP/2 ssl proxy cache. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; +use Test::Nginx::HTTP2; + +use POSIX qw/ ceil /; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +my $t = Test::Nginx->new() + ->has(qw/http http_ssl http_v2 proxy cache socket_ssl/)->plan(10) + ->has_daemon('openssl'); + +$t->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + proxy_cache_path %%TESTDIR%%/cache keys_zone=NAME:1m; + proxy_cache_path %%TESTDIR%%/cache2 keys_zone=NAME2:1m; + + map $arg_slow $rate { + default 8k; + 1 90; + } + + server { + listen 127.0.0.1:8080 http2 ssl; + server_name localhost; + error_log %%TESTDIR%%/error8080.log debug; + + ssl_certificate_key localhost.key; + ssl_certificate localhost.crt; + + location / { + proxy_pass http://127.0.0.1:8081; + proxy_cache NAME; + proxy_http_version 1.1; + proxy_cache_revalidate on; + } + } + + server { + listen 127.0.0.1:8081; + server_name localhost; + error_log %%TESTDIR%%/error8081.log debug; + + location / { + proxy_pass http://127.0.0.1:8082; + proxy_cache NAME2; + proxy_http_version 1.1; + proxy_cache_revalidate on; + } + } + + server { + listen 127.0.0.1:8082; + server_name localhost; + error_log %%TESTDIR%%/error8082.log debug; + + location / { + add_header Cache-Control s-maxage=$arg_ttl; + limit_rate $rate; + } + } +} + +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->write_file('t.html', 'SEE-THIS'); + +open OLDERR, ">&", \*STDERR; close STDERR; +$t->run(); +open STDERR, ">&", \*OLDERR; + +############################################################################### + +my $s = getconn(port(8080)); +ok($s, 'ssl connection'); + +my ($path, $sid, $frames, $frame, $t1, $resident_time); + +# normal origin + +wait_until_next_second(); + +$path = '/t.html?ttl=2'; + +$sid = $s->new_stream({ path => $path }); +$frames = $s->read(all => [{ sid => $sid, fin => 1 }]); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{headers}->{'age'}, undef, 'age first'); + +select undef, undef, undef, 2.0; + +$sid = $s->new_stream({ path => $path }); +$frames = $s->read(all => [{ sid => $sid, fin => 1 }]); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{headers}->{'age'}, 2, 'age hit'); + +select undef, undef, undef, 1.0; + +$sid = $s->new_stream({ path => $path }); +$frames = $s->read(all => [{ sid => $sid, fin => 1 }]); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{headers}->{'age'}, undef, 'age updated'); + +# slow origin + +SKIP: { +skip 'no exec on win32', 3 if $^O eq 'MSWin32'; + +wait_until_next_second(); + +$path = '/t.html?ttl=4&slow=1'; + +$sid = $s->new_stream({ path => $path }); +$frames = $s->read(all => [{ sid => $sid, fin => 1 }]); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{headers}->{'age'}, 2, 'slow origin first'); + +select undef, undef, undef, 2.0; + +$sid = $s->new_stream({ path => $path }); +$frames = $s->read(all => [{ sid => $sid, fin => 1 }]); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{headers}->{'age'}, 4, 'slow origin hit'); + +select undef, undef, undef, 1.0; + +$sid = $s->new_stream({ path => $path }); +$frames = $s->read(all => [{ sid => $sid, fin => 1 }]); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{headers}->{'age'}, 2, 'slow origin updated'); + +} + +# update age after restart + +$path = '/t.html?ttl=20'; + +$sid = $s->new_stream({ path => $path }); +$frames = $s->read(all => [{ sid => $sid, fin => 1 }]); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{headers}->{'age'}, undef, 'age before restart'); +$t1 = time(); + +$t->stop(); + +select undef, undef, undef, 1.0; + +open OLDERR, ">&", \*STDERR; close STDERR; +$t->run(); +open STDERR, ">&", \*OLDERR; + +$resident_time = time() - $t1; + +$s = getconn(port(8080)); +ok($s, 'ssl connection'); + +$sid = $s->new_stream({ path => $path }); +$frames = $s->read(all => [{ sid => $sid, fin => 1 }]); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{headers}->{'age'}, $resident_time, 'age after restart'); + +$t->stop(); + +############################################################################### + +sub getconn { + my ($port) = @_; + my $s; + + eval { + my $sock = Test::Nginx::HTTP2::new_socket($port, SSL => 1, + alpn => 'h2'); + $s = Test::Nginx::HTTP2->new($port, socket => $sock) + if $sock->alpn_selected(); + }; + + return $s if defined $s; + + eval { + my $sock = Test::Nginx::HTTP2::new_socket($port, SSL => 1, + npn => 'h2'); + $s = Test::Nginx::HTTP2->new($port, socket => $sock) + if $sock->next_proto_negotiated(); + }; + + return $s; +} + +# Wait until the next second boundary. +# Calling this before sending a request increases the likelihood that the +# timestamp value does not cross into the next second while sending a request +# and receiving a response. +sub wait_until_next_second { + my $now = time(); + my $next_second = ceil($now); + my $sleep = $next_second - $now; + select undef, undef, undef, $sleep; +} + +############################################################################### diff -r a1e4c426f106 -r 758a6c4c1bab h3_proxy_cache_age.t --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/h3_proxy_cache_age.t Tue Jun 25 22:34:16 2024 +0900 @@ -0,0 +1,215 @@ +#!/usr/bin/perl + +# (C) Sergey Kandaurov +# (C) Nginx, Inc. +# (C) Hiroaki Nakamura + +# Tests for age in HTTP/3 proxy cache. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; +use Test::Nginx::HTTP3; + +use POSIX qw/ ceil /; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +my $t = Test::Nginx->new()->has(qw/http http_v3 proxy cryptx/) + ->has_daemon('openssl')->plan(8) + ->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + ssl_certificate_key localhost.key; + ssl_certificate localhost.crt; + + log_format test $uri:$status:$request_completion; + + proxy_cache_path %%TESTDIR%%/cache keys_zone=NAME:1m; + + map $arg_slow $rate { + default 8k; + 1 90; + } + + server { + listen 127.0.0.1:%%PORT_8980_UDP%% quic; + server_name localhost; + error_log %%TESTDIR%%/error8080.log debug; + + location / { + proxy_pass http://127.0.0.1:8081/; + proxy_cache NAME; + proxy_http_version 1.1; + proxy_cache_revalidate on; + } + } + + server { + listen 127.0.0.1:8081; + server_name localhost; + error_log %%TESTDIR%%/error8081.log debug; + + location / { + proxy_pass http://127.0.0.1:8082; + proxy_cache NAME; + proxy_http_version 1.1; + proxy_cache_revalidate on; + } + } + + server { + listen 127.0.0.1:8082; + server_name localhost; + error_log %%TESTDIR%%/error8082.log debug; + + location / { + add_header Cache-Control s-maxage=$arg_ttl; + limit_rate $rate; + } + } +} + +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"; +} + +my $content = 'SEE-THIS'; +$t->write_file('t.html', $content); +$t->run(); + +############################################################################### + +my $s = Test::Nginx::HTTP3->new(); + +my ($path, $sid, $frames, $frame, $t1, $resident_time); + +# normal origin + +wait_until_next_second(); + +$path = '/t.html?ttl=2'; + +$sid = $s->new_stream({ path => $path }); +$frames = $s->read(all => [{ sid => $sid, fin => 1 }]); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{headers}->{'age'}, undef, 'age first'); + +select undef, undef, undef, 2.0; + +$sid = $s->new_stream({ path => $path }); +$frames = $s->read(all => [{ sid => $sid, fin => 1 }]); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{headers}->{'age'}, 2, 'age hit'); + +select undef, undef, undef, 1.0; + +$sid = $s->new_stream({ path => $path }); +$frames = $s->read(all => [{ sid => $sid, fin => 1 }]); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{headers}->{'age'}, undef, 'age updated'); + +# slow origin + +wait_until_next_second(); + +$path = '/t.html?ttl=4&slow=1'; + +$sid = $s->new_stream({ path => $path }); +$frames = $s->read(all => [{ sid => $sid, fin => 1 }]); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{headers}->{'age'}, 2, 'slow origin first'); + +select undef, undef, undef, 2.0; + +$sid = $s->new_stream({ path => $path }); +$frames = $s->read(all => [{ sid => $sid, fin => 1 }]); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{headers}->{'age'}, 4, 'slow origin hit'); + +select undef, undef, undef, 1.0; + +$sid = $s->new_stream({ path => $path }); +$frames = $s->read(all => [{ sid => $sid, fin => 1 }]); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{headers}->{'age'}, 2, 'slow origin updated'); + +# update age after restart + +$path = '/t.html?ttl=20'; + +$sid = $s->new_stream({ path => $path }); +$frames = $s->read(all => [{ sid => $sid, fin => 1 }]); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{headers}->{'age'}, undef, 'age before restart'); +$t1 = time(); + +$t->stop(); + +select undef, undef, undef, 1.0; + +open OLDERR, ">&", \*STDERR; close STDERR; +$t->run(); +open STDERR, ">&", \*OLDERR; + +$resident_time = time() - $t1; + +$s = Test::Nginx::HTTP3->new(); + +$sid = $s->new_stream({ path => $path }); +$frames = $s->read(all => [{ sid => $sid, fin => 1 }]); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{headers}->{'age'}, $resident_time, 'age after restart'); + +$t->stop(); + +############################################################################### + +# Wait until the next second boundary. +# Calling this before sending a request increases the likelihood that the +# timestamp value does not cross into the next second while sending a request +# and receiving a response. +sub wait_until_next_second { + my $now = time(); + my $next_second = ceil($now); + my $sleep = $next_second - $now; + select undef, undef, undef, $sleep; +} + +############################################################################### diff -r a1e4c426f106 -r 758a6c4c1bab proxy_cache_age.t --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/proxy_cache_age.t Tue Jun 25 22:34:16 2024 +0900 @@ -0,0 +1,183 @@ +#!/usr/bin/perl + +# (C) Maxim Dounin +# (C) Hiroaki Nakamura + +# Tests for age in http proxy cache. + +############################################################################### + +use warnings; +use strict; + +use Test::More; +use Socket qw/ CRLF /; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; + +use POSIX qw/ ceil /; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +my $t = Test::Nginx->new()->has(qw/http proxy cache/)->plan(8) + ->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + proxy_cache_path %%TESTDIR%%/cache levels=1:2 + keys_zone=NAME:1m; + proxy_cache_path %%TESTDIR%%/cache2 levels=1:2 + keys_zone=NAME2:1m; + + map $arg_slow $rate { + default 8k; + 1 100; + } + + server { + listen 127.0.0.1:8080; + server_name localhost; + error_log %%TESTDIR%%/error8080.log debug; + + location / { + proxy_pass http://127.0.0.1:8081; + proxy_cache NAME; + proxy_http_version 1.1; + proxy_cache_revalidate on; + add_header parent_date $upstream_http_date; + add_header child_msec $msec; + } + } + + server { + listen 127.0.0.1:8081; + server_name localhost; + error_log %%TESTDIR%%/error8081.log debug; + + location / { + proxy_pass http://127.0.0.1:8082; + proxy_cache NAME2; + proxy_http_version 1.1; + proxy_cache_revalidate on; + add_header origin_date $upstream_http_date; + add_header parent_msec $msec; + } + } + + server { + listen 127.0.0.1:8082; + server_name localhost; + error_log %%TESTDIR%%/error8082.log debug; + + location / { + add_header Cache-Control $http_x_cache_control; + limit_rate $rate; + add_header origin_msec $msec; + } + } +} + +EOF + +$t->write_file('t.html', 'SEE-THIS'); +$t->write_file('t2.html', 'SEE-THIS'); +$t->write_file('t3.html', 'SEE-THIS'); + +$t->run(); + +############################################################################### + +# normal origin + +wait_until_next_second(); + +unlike(get('/t.html', 's-maxage=2'), qr/\r\nAge: /, 'age first'); + +sleep 2; + +like(get('/t.html', 's-maxage=2'), qr/\r\nAge: 2\r\n/, 'age hit'); + +sleep 1; + +unlike(http_get('/t.html'), qr/\r\nAge: /, 'age updated'); + +# slow origin + +SKIP: { +skip 'no exec on win32', 3 if $^O eq 'MSWin32'; + +wait_until_next_second(); + +like(get('/t2.html?slow=1', 's-maxage=4'), qr/\r\nAge: 2\r\n/, + 'slow origin first'); + +sleep 2; + +like(http_get('/t2.html?slow=1'), qr/\r\nAge: 4\r\n/, 'slow origin hit'); + +sleep 1; + +like(http_get('/t2.html?slow=1'), qr/\r\nAge: 2\r\n/, 'slow origin updated'); + +} + +# update age after restart + +wait_until_next_second(); + +unlike(get('/t3.html', 's-maxage=20'), qr/\r\nAge: /, 'age before restart'); +my $t1 = time(); + +$t->stop(); + +sleep 1; + +$t->run(); + +my $resident_time = time() - $t1; +like(http_get('/t3.html'), qr/\r\nAge: $resident_time\r\n/, + 'age after restart'); + +# is(1, 0, 'intentional error'); + +$t->stop(); + +############################################################################### + +sub get { + my ($url, $extra, %extra) = @_; + return http(<<EOF, %extra); +GET $url HTTP/1.1 +Host: localhost +Connection: close +X-Cache-Control: $extra + +EOF +} + +# Wait until the next second boundary. +# Calling this before sending a request increases the likelihood that the +# timestamp value does not cross into the next second while sending a request +# and receiving a response. +sub wait_until_next_second { + my $now = time(); + my $next_second = ceil($now); + my $sleep = $next_second - $now; + select undef, undef, undef, $sleep; +} + +############################################################################### diff -r a1e4c426f106 -r 758a6c4c1bab proxy_cache_use_stale.t --- a/proxy_cache_use_stale.t Tue Jun 25 22:34:07 2024 +0900 +++ b/proxy_cache_use_stale.t Tue Jun 25 22:34:16 2024 +0900 @@ -206,7 +206,8 @@ like(http_get('/regexp.html'), qr/HIT/, $t->write_file('t6.html', 'SEE-THAT'); -my $s = get('/t6.html', 'max-age=1, stale-while-revalidate=2', start => 1); +# max-age must be 5 here since response delay is 4 seconds. +my $s = get('/t6.html', 'max-age=5, stale-while-revalidate=2', start => 1); select undef, undef, undef, 0.2; like(http_get('/t6.html'), qr/UPDATING.*SEE-THIS/s, 's-w-r - updating'); like(http_end($s), qr/STALE.*SEE-THIS/s, 's-w-r - updating stale');