diff 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
new file mode 100644
--- /dev/null
+++ b/body_discard.t
@@ -0,0 +1,449 @@
+#!/usr/bin/perl
+
+# (C) Maxim Dounin
+
+# Tests for discarding 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 addition memcached/);
+
+plan(skip_all => 'not yet') unless $t->has_version('1.27.0');
+
+$t->plan(33)->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;
+
+        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 1;
+            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 /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(http(
+	'POST / HTTP/1.0' . CRLF .
+	'Content-Length: 10000' . CRLF . CRLF .
+	'0123456789'
+), qr/ 413 .*custom error 413/s, 'custom error 413');
+
+# subrequest after discarding body
+
+like(http(
+	'GET /add HTTP/1.0' . CRLF . CRLF
+), qr/backend body:::.*main response/s, 'add');
+
+like(http(
+	'POST /add HTTP/1.0' . CRLF .
+	'Content-Length: 10' . CRLF . CRLF .
+	'0123456789'
+), qr/backend body:::.*main response/s, 'add small');
+
+like(http(
+	'POST /add HTTP/1.0' . CRLF .
+	'Content-Length: 10000' . CRLF . CRLF .
+	'0123456789'
+), qr/backend body:::.*main response/s, 'add long');
+
+like(http(
+	'POST /add HTTP/1.1' . CRLF .
+	'Host: localhost' . CRLF .
+	'Connection: close' . CRLF .
+	'Transfer-Encoding: chunked' . CRLF . CRLF .
+	'a' . CRLF .
+	'0123456789' . CRLF .
+	'0' . CRLF . CRLF
+), qr/backend body:::.*main response/s, 'add chunked');
+
+like(http(
+	'POST /add HTTP/1.1' . CRLF .
+	'Host: localhost' . CRLF .
+	'Connection: close' . CRLF .
+	'Transfer-Encoding: chunked' . CRLF . CRLF .
+	'1' . CRLF .
+	'X' . CRLF .
+	'9' . CRLF .
+	'123456789' . CRLF .
+	'0' . CRLF . CRLF
+), qr/backend body:::.*main response/s, 'add chunked multi');
+
+like(http(
+	'POST /add HTTP/1.1' . CRLF .
+	'Host: localhost' . CRLF .
+	'Connection: close' . CRLF .
+	'Transfer-Encoding: chunked' . CRLF . CRLF .
+	'ffff' . CRLF .
+	'0123456789'
+), qr/backend body:::.*main response/s, 'add chunked long');
+
+# error_page 502 with proxy_pass after discarding body
+
+like(http(
+	'GET /memcached HTTP/1.0' . CRLF . CRLF
+), qr/ 502 .*backend body:::/s, 'memcached');
+
+like(http(
+	'GET /memcached HTTP/1.0' . CRLF .
+	'Content-Length: 10' . CRLF . CRLF .
+	'0123456789'
+), qr/ 502 .*backend body:::/s, 'memcached small');
+
+like(http(
+	'GET /memcached HTTP/1.0' . CRLF .
+	'Content-Length: 10000' . CRLF . CRLF .
+	'0123456789'
+), qr/ 502 .*backend body:::/s, 'memcached long');
+
+like(http(
+	'GET /memcached HTTP/1.1' . CRLF .
+	'Host: localhost' . CRLF .
+	'Connection: close' . CRLF .
+	'Transfer-Encoding: chunked' . CRLF . CRLF .
+	'a' . CRLF .
+	'0123456789' . CRLF .
+	'0' . CRLF . CRLF
+), qr/ 502 .*backend body:::/s, 'memcached chunked');
+
+like(http(
+	'GET /memcached HTTP/1.1' . CRLF .
+	'Host: localhost' . CRLF .
+	'Connection: close' . CRLF .
+	'Transfer-Encoding: chunked' . CRLF . CRLF .
+	'1' . CRLF .
+	'X' . CRLF .
+	'9' . CRLF .
+	'123456789' . CRLF .
+	'0' . CRLF . CRLF
+), qr/ 502 .*backend body:::/s, 'memcached chunked multi');
+
+like(http(
+	'GET /memcached HTTP/1.1' . CRLF .
+	'Host: localhost' . CRLF .
+	'Connection: close' . CRLF .
+	'Transfer-Encoding: chunked' . CRLF . CRLF .
+	'ffff' . CRLF .
+	'0123456789'
+), qr/ 502 .*backend body:::/s, 'memcached chunked long');
+
+# error_page 413 with proxy_pass
+
+like(http(
+	'GET /proxy HTTP/1.0' . CRLF . CRLF
+), qr/ 502 .*backend body:::/s, 'proxy');
+
+like(http(
+	'POST /proxy HTTP/1.0' . CRLF .
+	'Content-Length: 10' . CRLF . CRLF .
+	'0123456789'
+), qr/ 413 .*backend body:::/s, 'proxy small');
+
+like(http(
+	'POST /proxy HTTP/1.0' . CRLF .
+	'Content-Length: 10000' . CRLF . CRLF .
+	'0123456789'
+), qr/ 413 .*backend body:::/s, 'proxy long');
+
+like(http(
+	'POST /proxy HTTP/1.1' . CRLF .
+	'Host: localhost' . CRLF .
+	'Connection: close' . CRLF .
+	'Transfer-Encoding: chunked' . CRLF . CRLF .
+	'a' . CRLF .
+	'0123456789' . CRLF .
+	'0' . CRLF . CRLF
+), qr/ 413 .*backend body:::/s, 'proxy chunked');
+
+like(http(
+	'POST /proxy HTTP/1.1' . CRLF .
+	'Host: localhost' . CRLF .
+	'Connection: close' . CRLF .
+	'Transfer-Encoding: chunked' . CRLF . CRLF .
+	'1' . CRLF .
+	'X' . CRLF .
+	'9' . CRLF .
+	'123456789' . CRLF .
+	'0' . CRLF . CRLF
+), qr/ 413 .*backend body:::/s, 'proxy chunked multi');
+
+like(http(
+	'POST /proxy HTTP/1.1' . CRLF .
+	'Host: localhost' . CRLF .
+	'Connection: close' . CRLF .
+	'Transfer-Encoding: chunked' . CRLF . CRLF .
+	'ffff' . CRLF .
+	'0123456789'
+), qr/ 413 .*backend body:::/s, 'proxy chunked long');
+
+# error_page 400 with proxy_pass
+
+# note that "chunked and length" test triggers 400 during parsing
+# request headers, and therefore needs error_page at server level
+
+like(http(
+	'POST /proxy HTTP/1.1' . CRLF .
+	'Host: localhost' . CRLF .
+	'Connection: close' . CRLF .
+	'Transfer-Encoding: chunked' . CRLF . CRLF .
+	'1' . CRLF .
+	'X' . CRLF .
+	'X' . CRLF
+), qr/ 400 .*backend body:::/s, 'proxy chunked bad');
+
+like(http(
+	'POST /proxy HTTP/1.1' . CRLF .
+	'Host: localhost' . CRLF .
+	'Connection: close' . CRLF .
+	'Content-Length: 10' . CRLF .
+	'Transfer-Encoding: chunked' . CRLF . CRLF .
+	'0' . CRLF . CRLF
+), qr/ 400 .*backend body:::/s, 'proxy chunked and length');
+
+# error_page 502 after proxy with request buffering disabled
+
+like(http(
+	'GET /unbuf HTTP/1.0' . CRLF . CRLF
+), qr/ 502 .*backend body:::/s, 'unbuf proxy');
+
+like(http(
+	'POST /unbuf HTTP/1.0' . CRLF .
+	'Content-Length: 10' . CRLF . CRLF .
+	'0',
+	sleep => 0.1,
+	body =>
+	'123456789'
+), qr/ 502 .*backend body:::/s, 'unbuf proxy small');
+
+like(http(
+	'POST /unbuf HTTP/1.0' . CRLF .
+	'Content-Length: 10000' . CRLF . CRLF .
+	'0123456789'
+), qr/ 502 .*backend body:::/s, 'unbuf proxy long');
+
+like(http(
+	'POST /unbuf HTTP/1.1' . CRLF .
+	'Host: localhost' . CRLF .
+	'Connection: close' . CRLF .
+	'Transfer-Encoding: chunked' . CRLF . CRLF,
+	sleep => 0.1,
+	body =>
+	'a' . CRLF .
+	'0123456789' . CRLF .
+	'0' . CRLF . CRLF
+), qr/ 502 .*backend body:::/s, 'unbuf proxy chunked');
+
+like(http(
+	'POST /unbuf HTTP/1.1' . CRLF .
+	'Host: localhost' . CRLF .
+	'Connection: close' . CRLF .
+	'Transfer-Encoding: chunked' . CRLF . CRLF .
+	'1' . CRLF .
+	'X' . CRLF,
+	sleep => 0.1,
+	body =>
+	'9' . CRLF .
+	'123456789' . CRLF .
+	'0' . CRLF . CRLF
+), qr/ 502 .*backend body:::/s, 'unbuf proxy chunked multi');
+
+like(http(
+	'POST /unbuf HTTP/1.1' . CRLF .
+	'Host: localhost' . CRLF .
+	'Connection: close' . CRLF .
+	'Transfer-Encoding: chunked' . CRLF . CRLF .
+	'ffff' . CRLF .
+	'0123456789'
+), qr/ 502 .*backend body:::/s, 'unbuf proxy chunked long');
+
+# error_page 413 and $content_length
+# (used in fastcgi_pass, grpc_pass, uwsgi_pass)
+
+like(http(
+	'GET /length HTTP/1.0' . CRLF . CRLF
+), qr/ 502 .*frontend body:::/s, '$content_length');
+
+like(http(
+	'POST /length HTTP/1.0' . CRLF .
+	'Content-Length: 10' . CRLF . CRLF .
+	'0123456789'
+), qr/ 413 .*frontend body:::/s, '$content_length small');
+
+like(http(
+	'POST /length HTTP/1.0' . CRLF .
+	'Content-Length: 10000' . CRLF . CRLF .
+	'0123456789'
+), qr/ 413 .*frontend body:::/s, '$content_length long');
+
+like(http(
+	'POST /length HTTP/1.1' . CRLF .
+	'Host: localhost' . CRLF .
+	'Connection: close' . CRLF .
+	'Transfer-Encoding: chunked' . CRLF . CRLF .
+	'a' . CRLF .
+	'0123456789' . CRLF .
+	'0' . CRLF . CRLF
+), qr/ 413 .*frontend body:::/s, '$content_length chunked');
+
+like(http(
+	'POST /length HTTP/1.1' . CRLF .
+	'Host: localhost' . CRLF .
+	'Connection: close' . CRLF .
+	'Transfer-Encoding: chunked' . CRLF . CRLF .
+	'1' . CRLF .
+	'X' . CRLF .
+	'9' . CRLF .
+	'123456789' . CRLF .
+	'0' . CRLF . CRLF
+), qr/ 413 .*frontend body:::/s, '$content_length chunked multi');
+
+like(http(
+	'POST /length HTTP/1.1' . CRLF .
+	'Host: localhost' . CRLF .
+	'Connection: close' . CRLF .
+	'Transfer-Encoding: chunked' . CRLF . CRLF .
+	'ffff' . CRLF .
+	'0123456789'
+), qr/ 413 .*frontend body:::/s, '$content_length chunked long');
+
+###############################################################################