Mercurial > hg > nginx-tests
diff h2_request_body_discard.t @ 1961:fe6f22da53ec
Tests: tests for usage of discarded body.
The client_max_body_size limit should be ignored when the request body
is already discarded. In HTTP/1.x, this is done by checking the
r->discard_body flag when the body is being discarded, and because
r->headers_in.content_length_n is 0 when it's already discarded. This,
however, does not happen with HTTP/2 and HTTP/3, and therefore
"error_page 413" does not work without relaxing the limit.
Further, with proxy_pass, r->headers_in.content_length_n is used to determine
length of the request body, and therefore is not correct if discarding of
the request body isn't yet complete. While discarding the request body,
r->headers_in.content_length_n contains the rest of the body to discard
(or, in case of chunked request body, the rest of the current chunk to
discard).
Similarly, the $content_length variable uses r->headers_in.content_length
if available, and also incorrect. The $content_length variable is used
when proxying with fastcgi_pass, grpc_pass, and uwsgi_pass (scgi_pass uses
the value calculated based on the actual request body buffers, and therefore
works correctly).
author | Maxim Dounin <mdounin@mdounin.ru> |
---|---|
date | Sat, 27 Apr 2024 18:55:50 +0300 |
parents | |
children |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/h2_request_body_discard.t Sat Apr 27 18:55:50 2024 +0300 @@ -0,0 +1,407 @@ +#!/usr/bin/perl + +# (C) Maxim Dounin + +# Tests for discarding request body with HTTP/2. + +############################################################################### + +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; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +my $t = Test::Nginx->new() + ->has(qw/http http_v2 proxy rewrite addition memcached/); + +plan(skip_all => 'not yet') unless $t->has_version('1.27.0'); + +$t->plan(38)->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; + + http2 on; + + lingering_timeout 1s; + add_header X-Body body:$content_length:$request_body:; + + client_max_body_size 1k; + + error_page 400 /proxy/error400; + + location / { + error_page 413 /error413; + proxy_pass http://127.0.0.1:8082; + } + + location /error413 { + return 200 "custom error 413"; + } + + location /add { + return 200 "main response"; + add_before_body /add/before; + addition_types *; + client_max_body_size 1m; + } + + location /add/before { + proxy_pass http://127.0.0.1:8081; + } + + location /memcached { + client_max_body_size 1m; + error_page 502 /memcached/error502; + memcached_pass 127.0.0.1:8083; + set $memcached_key $request_uri; + } + + location /memcached/error502 { + proxy_pass http://127.0.0.1:8081; + } + + location /proxy { + client_max_body_size 3; + error_page 413 /proxy/error413; + error_page 400 /proxy/error400; + error_page 502 /proxy/error502; + proxy_pass http://127.0.0.1:8083; + } + + location /proxy/error413 { + proxy_pass http://127.0.0.1:8081; + } + + location /proxy/error400 { + proxy_pass http://127.0.0.1:8081; + } + + location /proxy/error502 { + proxy_pass http://127.0.0.1:8081; + } + + location /unbuf { + client_max_body_size 1m; + error_page 502 /unbuf/error502; + proxy_pass http://127.0.0.1:8083; + proxy_request_buffering off; + proxy_http_version 1.1; + } + + location /unbuf/error502 { + client_max_body_size 1m; + proxy_pass http://127.0.0.1:8081; + } + + location /unbuf2 { + client_max_body_size 1m; + error_page 400 /unbuf2/error400; + proxy_pass http://127.0.0.1:8081; + proxy_request_buffering off; + proxy_http_version 1.1; + } + + location /unbuf2/error400 { + client_max_body_size 1m; + proxy_pass http://127.0.0.1:8081; + } + + location /length { + client_max_body_size 1; + error_page 413 /length/error413; + error_page 502 /length/error502; + proxy_pass http://127.0.0.1:8083; + } + + location /length/error413 { + return 200 "frontend body:$content_length:$request_body:"; + } + + location /length/error502 { + return 200 "frontend body:$content_length:$request_body:"; + } + } + + server { + listen 127.0.0.1:8081; + server_name localhost; + + location / { + proxy_pass http://127.0.0.1:8082; + proxy_set_header X-Body body:$content_length:$request_body:; + } + } + + server { + listen 127.0.0.1:8082; + server_name localhost; + + return 200 "backend $http_x_body"; + } + + server { + listen 127.0.0.1:8083; + server_name localhost; + + return 444; + } +} + +EOF + +$t->run(); + +############################################################################### + +# error_page 413 should work without redefining client_max_body_size + +like(http2_get_body('/', '0123456789' x 128), + qr/status: 413.*custom error 413/s, 'custom error 413'); + +# subrequest after discarding body + +like(http2_get('/add'), + qr/backend body:::.*main response/s, 'add'); +like(http2_get_body('/add', '0123456789'), + qr/backend body:::.*main response/s, 'add small'); +like(http2_get_body_incomplete('/add', 10000, '0123456789'), + qr/backend body:::.*main response/s, 'add long'); +like(http2_get_body_nolen('/add', '0123456789'), + qr/backend body:::.*main response/s, 'add nolen'); +like(http2_get_body_nolen('/add', '0', '123456789'), + qr/backend body:::.*main response/s, 'add nolen multi'); +like(http2_get_body_incomplete_nolen('/add', 10000, '0123456789'), + qr/backend body:::.*main response/s, 'add chunked long'); + +# error_page 502 with proxy_pass after discarding body + +like(http2_get('/memcached'), + qr/backend body:::/s, 'memcached'); +like(http2_get_body('/memcached', '0123456789'), + qr/status: 502.*backend body:::/s, 'memcached small'); +like(http2_get_body_incomplete('/memcached', 10000, '0123456789'), + qr/status: 502.*backend body:::/s, 'memcached long'); +like(http2_get_body_nolen('/memcached', '0123456789'), + qr/status: 502.*backend body:::/s, 'memcached nolen'); +like(http2_get_body_nolen('/memcached', '0', '123456789'), + qr/status: 502.*backend body:::/s, 'memcached nolen multi'); +like(http2_get_body_incomplete_nolen('/memcached', 10000, '0123456789'), + qr/status: 502.*backend body:::/s, 'memcached nolen long'); + +# error_page 413 with proxy_pass + +like(http2_get('/proxy'), + qr/status: 502.*backend body:::/s, 'proxy'); +like(http2_get_body('/proxy', '0123456789'), + qr/status: 413.*backend body:::/s, 'proxy small'); +like(http2_get_body_incomplete('/proxy', 10000, '0123456789'), + qr/status: 413.*backend body:::/s, 'proxy long'); +like(http2_get_body_nolen('/proxy', '0123456789'), + qr/status: 413.*backend body:::/s, 'proxy nolen'); +like(http2_get_body_nolen('/proxy', '0', '123456789'), + qr/status: 413.*backend body:::/s, 'proxy nolen multi'); +like(http2_get_body_incomplete_nolen('/proxy', 10000, '0123456789'), + qr/status: 413.*backend body:::/s, 'proxy nolen long'); + +# error_page 400 with proxy_pass + +# note that "proxy too short" test triggers 400 during parsing +# request headers, and therefore needs error_page at server level + +like(http2_get_body_custom('/proxy', 1), + qr/status: 400.*backend body:::/s, 'proxy too short'); +like(http2_get_body_custom('/proxy', 1, ''), + qr/status: 400.*backend body:::/s, 'proxy too short body'); +like(http2_get_body_custom('/proxy', 1, '01'), + qr/status: 400.*backend body:::/s, 'proxy too long'); +like(http2_get_body_custom('/proxy', 1, '01', more => 1), + qr/status: 400.*backend body:::/s, 'proxy too long more'); + +# error_page 502 after proxy with request buffering disabled + +like(http2_get('/unbuf'), + qr/status: 502.*backend body:::/s, 'unbuf proxy'); +like(http2_get_body('/unbuf', '0123456789'), + qr/status: 502.*backend body:::/s, 'unbuf proxy small'); +like(http2_get_body_incomplete('/unbuf', 10000, '0123456789'), + qr/status: 502.*backend body:::/s, 'unbuf proxy long'); +like(http2_get_body_nolen('/unbuf', '0123456789'), + qr/status: 502.*backend body:::/s, 'unbuf proxy nolen'); +like(http2_get_body_nolen('/unbuf', '0', '123456789'), + qr/status: 502.*backend body:::/s, 'unbuf proxy nolen multi'); +like(http2_get_body_incomplete_nolen('/unbuf', 10000, '0123456789'), + qr/status: 502.*backend body:::/s, 'unbuf proxy nolen long'); + +# error_page 400 after proxy with request buffering disabled + +like(http2_get_body_custom('/unbuf2', 1, '', sleep => 0.1), + qr/status: 400.*backend body:::/s, 'unbuf too short'); +like(http2_get_body_custom('/unbuf2', 1, '01', sleep => 0.1), + qr/status: 400.*backend body:::/s, 'unbuf too long'); +like(http2_get_body_custom('/unbuf2', 1, '01', sleep => 0.1, more => 1), + qr/status: 400.*backend body:::/s, 'unbuf too long more'); + +# error_page 413 and $content_length +# (used in fastcgi_pass, grpc_pass, uwsgi_pass) + +like(http2_get('/length'), + qr/status: 502.*frontend body:::/s, '$content_length'); +like(http2_get_body('/length', '0123456789'), + qr/status: 413.*frontend body:::/s, '$content_length small'); +like(http2_get_body_incomplete('/length', 10000, '0123456789'), + qr/status: 413.*frontend body:::/s, '$content_length long'); +like(http2_get_body_nolen('/length', '0123456789'), + qr/status: 413.*frontend body:::/s, '$content_length nolen'); +like(http2_get_body_nolen('/length', '0', '123456789'), + qr/status: 413.*frontend body:::/s, '$content_length nolen multi'); +like(http2_get_body_incomplete_nolen('/length', 10000, '0123456789'), + qr/status: 413.*frontend body:::/s, '$content_length nolen long'); + +############################################################################### + +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; + my ($data) = grep { $_->{type} eq "DATA" } @$frames; + + return join("\n", map { "$_: " . $frame->{headers}->{$_}; } + keys %{$frame->{headers}}) . "\n\n" . $data->{data}; +} + +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; + my ($data) = grep { $_->{type} eq "DATA" } @$frames; + + return join("\n", map { "$_: " . $frame->{headers}->{$_}; } + keys %{$frame->{headers}}) . "\n\n" . $data->{data}; +} + +sub http2_get_body_nolen { + my ($uri, $body, $body2) = @_; + + my $s = Test::Nginx::HTTP2->new(); + my $sid = $s->new_stream({ path => $uri, body_more => 1 }); + + if (defined $body2) { + $s->h2_body($body, { body_more => 1 }); + $s->h2_body($body2); + } else { + $s->h2_body($body); + } + + my $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); + + my ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; + my ($data) = grep { $_->{type} eq "DATA" } @$frames; + + return join("\n", map { "$_: " . $frame->{headers}->{$_}; } + keys %{$frame->{headers}}) . "\n\n" . $data->{data}; +} + +sub http2_get_body_incomplete { + my ($uri, $len, $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 => $len }, + ], + body_more => 1 + }); + $s->h2_body($body, { body_more => 1 }); + + my $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); + + my ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; + my ($data) = grep { $_->{type} eq "DATA" } @$frames; + + return join("\n", map { "$_: " . $frame->{headers}->{$_}; } + keys %{$frame->{headers}}) . "\n\n" . $data->{data}; +} + +sub http2_get_body_incomplete_nolen { + my ($uri, $len, $body) = @_; + + my $s = Test::Nginx::HTTP2->new(); + my $sid = $s->new_stream({ path => $uri, body_more => 1 }); + $s->h2_body($body, { body_more => 1 }); + + my $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); + + my ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; + my ($data) = grep { $_->{type} eq "DATA" } @$frames; + + return join("\n", map { "$_: " . $frame->{headers}->{$_}; } + keys %{$frame->{headers}}) . "\n\n" . $data->{data}; +} + +sub http2_get_body_custom { + my ($uri, $len, $body, %extra) = @_; + + 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 => $len }, + ], + body_more => (defined $body ? 1 : undef) + }); + + if (defined $body) { + select undef, undef, undef, $extra{sleep} if $extra{sleep}; + $s->h2_body($body, { body_more => 1 }); + $s->h2_body('') unless $extra{more}; + } + + my $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); + + my ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; + my ($data) = grep { $_->{type} eq "DATA" } @$frames; + + return join("\n", map { "$_: " . $frame->{headers}->{$_}; } + keys %{$frame->{headers}}) . "\n\n" . $data->{data}; +} + +###############################################################################