comparison 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
comparison
equal deleted inserted replaced
1960:e44ee916b959 1961:fe6f22da53ec
1 #!/usr/bin/perl
2
3 # (C) Maxim Dounin
4
5 # Tests for discarding request body with HTTP/2.
6
7 ###############################################################################
8
9 use warnings;
10 use strict;
11
12 use Test::More;
13 use Socket qw/ CRLF /;
14
15 BEGIN { use FindBin; chdir($FindBin::Bin); }
16
17 use lib 'lib';
18 use Test::Nginx;
19 use Test::Nginx::HTTP2;
20
21 ###############################################################################
22
23 select STDERR; $| = 1;
24 select STDOUT; $| = 1;
25
26 my $t = Test::Nginx->new()
27 ->has(qw/http http_v2 proxy rewrite addition memcached/);
28
29 plan(skip_all => 'not yet') unless $t->has_version('1.27.0');
30
31 $t->plan(38)->write_file_expand('nginx.conf', <<'EOF');
32
33 %%TEST_GLOBALS%%
34
35 daemon off;
36
37 events {
38 }
39
40 http {
41 %%TEST_GLOBALS_HTTP%%
42
43 server {
44 listen 127.0.0.1:8080;
45 server_name localhost;
46
47 http2 on;
48
49 lingering_timeout 1s;
50 add_header X-Body body:$content_length:$request_body:;
51
52 client_max_body_size 1k;
53
54 error_page 400 /proxy/error400;
55
56 location / {
57 error_page 413 /error413;
58 proxy_pass http://127.0.0.1:8082;
59 }
60
61 location /error413 {
62 return 200 "custom error 413";
63 }
64
65 location /add {
66 return 200 "main response";
67 add_before_body /add/before;
68 addition_types *;
69 client_max_body_size 1m;
70 }
71
72 location /add/before {
73 proxy_pass http://127.0.0.1:8081;
74 }
75
76 location /memcached {
77 client_max_body_size 1m;
78 error_page 502 /memcached/error502;
79 memcached_pass 127.0.0.1:8083;
80 set $memcached_key $request_uri;
81 }
82
83 location /memcached/error502 {
84 proxy_pass http://127.0.0.1:8081;
85 }
86
87 location /proxy {
88 client_max_body_size 3;
89 error_page 413 /proxy/error413;
90 error_page 400 /proxy/error400;
91 error_page 502 /proxy/error502;
92 proxy_pass http://127.0.0.1:8083;
93 }
94
95 location /proxy/error413 {
96 proxy_pass http://127.0.0.1:8081;
97 }
98
99 location /proxy/error400 {
100 proxy_pass http://127.0.0.1:8081;
101 }
102
103 location /proxy/error502 {
104 proxy_pass http://127.0.0.1:8081;
105 }
106
107 location /unbuf {
108 client_max_body_size 1m;
109 error_page 502 /unbuf/error502;
110 proxy_pass http://127.0.0.1:8083;
111 proxy_request_buffering off;
112 proxy_http_version 1.1;
113 }
114
115 location /unbuf/error502 {
116 client_max_body_size 1m;
117 proxy_pass http://127.0.0.1:8081;
118 }
119
120 location /unbuf2 {
121 client_max_body_size 1m;
122 error_page 400 /unbuf2/error400;
123 proxy_pass http://127.0.0.1:8081;
124 proxy_request_buffering off;
125 proxy_http_version 1.1;
126 }
127
128 location /unbuf2/error400 {
129 client_max_body_size 1m;
130 proxy_pass http://127.0.0.1:8081;
131 }
132
133 location /length {
134 client_max_body_size 1;
135 error_page 413 /length/error413;
136 error_page 502 /length/error502;
137 proxy_pass http://127.0.0.1:8083;
138 }
139
140 location /length/error413 {
141 return 200 "frontend body:$content_length:$request_body:";
142 }
143
144 location /length/error502 {
145 return 200 "frontend body:$content_length:$request_body:";
146 }
147 }
148
149 server {
150 listen 127.0.0.1:8081;
151 server_name localhost;
152
153 location / {
154 proxy_pass http://127.0.0.1:8082;
155 proxy_set_header X-Body body:$content_length:$request_body:;
156 }
157 }
158
159 server {
160 listen 127.0.0.1:8082;
161 server_name localhost;
162
163 return 200 "backend $http_x_body";
164 }
165
166 server {
167 listen 127.0.0.1:8083;
168 server_name localhost;
169
170 return 444;
171 }
172 }
173
174 EOF
175
176 $t->run();
177
178 ###############################################################################
179
180 # error_page 413 should work without redefining client_max_body_size
181
182 like(http2_get_body('/', '0123456789' x 128),
183 qr/status: 413.*custom error 413/s, 'custom error 413');
184
185 # subrequest after discarding body
186
187 like(http2_get('/add'),
188 qr/backend body:::.*main response/s, 'add');
189 like(http2_get_body('/add', '0123456789'),
190 qr/backend body:::.*main response/s, 'add small');
191 like(http2_get_body_incomplete('/add', 10000, '0123456789'),
192 qr/backend body:::.*main response/s, 'add long');
193 like(http2_get_body_nolen('/add', '0123456789'),
194 qr/backend body:::.*main response/s, 'add nolen');
195 like(http2_get_body_nolen('/add', '0', '123456789'),
196 qr/backend body:::.*main response/s, 'add nolen multi');
197 like(http2_get_body_incomplete_nolen('/add', 10000, '0123456789'),
198 qr/backend body:::.*main response/s, 'add chunked long');
199
200 # error_page 502 with proxy_pass after discarding body
201
202 like(http2_get('/memcached'),
203 qr/backend body:::/s, 'memcached');
204 like(http2_get_body('/memcached', '0123456789'),
205 qr/status: 502.*backend body:::/s, 'memcached small');
206 like(http2_get_body_incomplete('/memcached', 10000, '0123456789'),
207 qr/status: 502.*backend body:::/s, 'memcached long');
208 like(http2_get_body_nolen('/memcached', '0123456789'),
209 qr/status: 502.*backend body:::/s, 'memcached nolen');
210 like(http2_get_body_nolen('/memcached', '0', '123456789'),
211 qr/status: 502.*backend body:::/s, 'memcached nolen multi');
212 like(http2_get_body_incomplete_nolen('/memcached', 10000, '0123456789'),
213 qr/status: 502.*backend body:::/s, 'memcached nolen long');
214
215 # error_page 413 with proxy_pass
216
217 like(http2_get('/proxy'),
218 qr/status: 502.*backend body:::/s, 'proxy');
219 like(http2_get_body('/proxy', '0123456789'),
220 qr/status: 413.*backend body:::/s, 'proxy small');
221 like(http2_get_body_incomplete('/proxy', 10000, '0123456789'),
222 qr/status: 413.*backend body:::/s, 'proxy long');
223 like(http2_get_body_nolen('/proxy', '0123456789'),
224 qr/status: 413.*backend body:::/s, 'proxy nolen');
225 like(http2_get_body_nolen('/proxy', '0', '123456789'),
226 qr/status: 413.*backend body:::/s, 'proxy nolen multi');
227 like(http2_get_body_incomplete_nolen('/proxy', 10000, '0123456789'),
228 qr/status: 413.*backend body:::/s, 'proxy nolen long');
229
230 # error_page 400 with proxy_pass
231
232 # note that "proxy too short" test triggers 400 during parsing
233 # request headers, and therefore needs error_page at server level
234
235 like(http2_get_body_custom('/proxy', 1),
236 qr/status: 400.*backend body:::/s, 'proxy too short');
237 like(http2_get_body_custom('/proxy', 1, ''),
238 qr/status: 400.*backend body:::/s, 'proxy too short body');
239 like(http2_get_body_custom('/proxy', 1, '01'),
240 qr/status: 400.*backend body:::/s, 'proxy too long');
241 like(http2_get_body_custom('/proxy', 1, '01', more => 1),
242 qr/status: 400.*backend body:::/s, 'proxy too long more');
243
244 # error_page 502 after proxy with request buffering disabled
245
246 like(http2_get('/unbuf'),
247 qr/status: 502.*backend body:::/s, 'unbuf proxy');
248 like(http2_get_body('/unbuf', '0123456789'),
249 qr/status: 502.*backend body:::/s, 'unbuf proxy small');
250 like(http2_get_body_incomplete('/unbuf', 10000, '0123456789'),
251 qr/status: 502.*backend body:::/s, 'unbuf proxy long');
252 like(http2_get_body_nolen('/unbuf', '0123456789'),
253 qr/status: 502.*backend body:::/s, 'unbuf proxy nolen');
254 like(http2_get_body_nolen('/unbuf', '0', '123456789'),
255 qr/status: 502.*backend body:::/s, 'unbuf proxy nolen multi');
256 like(http2_get_body_incomplete_nolen('/unbuf', 10000, '0123456789'),
257 qr/status: 502.*backend body:::/s, 'unbuf proxy nolen long');
258
259 # error_page 400 after proxy with request buffering disabled
260
261 like(http2_get_body_custom('/unbuf2', 1, '', sleep => 0.1),
262 qr/status: 400.*backend body:::/s, 'unbuf too short');
263 like(http2_get_body_custom('/unbuf2', 1, '01', sleep => 0.1),
264 qr/status: 400.*backend body:::/s, 'unbuf too long');
265 like(http2_get_body_custom('/unbuf2', 1, '01', sleep => 0.1, more => 1),
266 qr/status: 400.*backend body:::/s, 'unbuf too long more');
267
268 # error_page 413 and $content_length
269 # (used in fastcgi_pass, grpc_pass, uwsgi_pass)
270
271 like(http2_get('/length'),
272 qr/status: 502.*frontend body:::/s, '$content_length');
273 like(http2_get_body('/length', '0123456789'),
274 qr/status: 413.*frontend body:::/s, '$content_length small');
275 like(http2_get_body_incomplete('/length', 10000, '0123456789'),
276 qr/status: 413.*frontend body:::/s, '$content_length long');
277 like(http2_get_body_nolen('/length', '0123456789'),
278 qr/status: 413.*frontend body:::/s, '$content_length nolen');
279 like(http2_get_body_nolen('/length', '0', '123456789'),
280 qr/status: 413.*frontend body:::/s, '$content_length nolen multi');
281 like(http2_get_body_incomplete_nolen('/length', 10000, '0123456789'),
282 qr/status: 413.*frontend body:::/s, '$content_length nolen long');
283
284 ###############################################################################
285
286 sub http2_get {
287 my ($uri) = @_;
288
289 my $s = Test::Nginx::HTTP2->new();
290 my $sid = $s->new_stream({ path => $uri });
291 my $frames = $s->read(all => [{ sid => $sid, fin => 1 }]);
292
293 my ($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
294 my ($data) = grep { $_->{type} eq "DATA" } @$frames;
295
296 return join("\n", map { "$_: " . $frame->{headers}->{$_}; }
297 keys %{$frame->{headers}}) . "\n\n" . $data->{data};
298 }
299
300 sub http2_get_body {
301 my ($uri, $body) = @_;
302
303 my $s = Test::Nginx::HTTP2->new();
304 my $sid = $s->new_stream({ path => $uri, body => $body });
305 my $frames = $s->read(all => [{ sid => $sid, fin => 1 }]);
306
307 my ($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
308 my ($data) = grep { $_->{type} eq "DATA" } @$frames;
309
310 return join("\n", map { "$_: " . $frame->{headers}->{$_}; }
311 keys %{$frame->{headers}}) . "\n\n" . $data->{data};
312 }
313
314 sub http2_get_body_nolen {
315 my ($uri, $body, $body2) = @_;
316
317 my $s = Test::Nginx::HTTP2->new();
318 my $sid = $s->new_stream({ path => $uri, body_more => 1 });
319
320 if (defined $body2) {
321 $s->h2_body($body, { body_more => 1 });
322 $s->h2_body($body2);
323 } else {
324 $s->h2_body($body);
325 }
326
327 my $frames = $s->read(all => [{ sid => $sid, fin => 1 }]);
328
329 my ($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
330 my ($data) = grep { $_->{type} eq "DATA" } @$frames;
331
332 return join("\n", map { "$_: " . $frame->{headers}->{$_}; }
333 keys %{$frame->{headers}}) . "\n\n" . $data->{data};
334 }
335
336 sub http2_get_body_incomplete {
337 my ($uri, $len, $body) = @_;
338
339 my $s = Test::Nginx::HTTP2->new();
340 my $sid = $s->new_stream({
341 headers => [
342 { name => ':method', value => 'GET' },
343 { name => ':scheme', value => 'http' },
344 { name => ':path', value => $uri },
345 { name => ':authority', value => 'localhost' },
346 { name => 'content-length', value => $len },
347 ],
348 body_more => 1
349 });
350 $s->h2_body($body, { body_more => 1 });
351
352 my $frames = $s->read(all => [{ sid => $sid, fin => 1 }]);
353
354 my ($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
355 my ($data) = grep { $_->{type} eq "DATA" } @$frames;
356
357 return join("\n", map { "$_: " . $frame->{headers}->{$_}; }
358 keys %{$frame->{headers}}) . "\n\n" . $data->{data};
359 }
360
361 sub http2_get_body_incomplete_nolen {
362 my ($uri, $len, $body) = @_;
363
364 my $s = Test::Nginx::HTTP2->new();
365 my $sid = $s->new_stream({ path => $uri, body_more => 1 });
366 $s->h2_body($body, { body_more => 1 });
367
368 my $frames = $s->read(all => [{ sid => $sid, fin => 1 }]);
369
370 my ($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
371 my ($data) = grep { $_->{type} eq "DATA" } @$frames;
372
373 return join("\n", map { "$_: " . $frame->{headers}->{$_}; }
374 keys %{$frame->{headers}}) . "\n\n" . $data->{data};
375 }
376
377 sub http2_get_body_custom {
378 my ($uri, $len, $body, %extra) = @_;
379
380 my $s = Test::Nginx::HTTP2->new();
381 my $sid = $s->new_stream({
382 headers => [
383 { name => ':method', value => 'GET' },
384 { name => ':scheme', value => 'http' },
385 { name => ':path', value => $uri },
386 { name => ':authority', value => 'localhost' },
387 { name => 'content-length', value => $len },
388 ],
389 body_more => (defined $body ? 1 : undef)
390 });
391
392 if (defined $body) {
393 select undef, undef, undef, $extra{sleep} if $extra{sleep};
394 $s->h2_body($body, { body_more => 1 });
395 $s->h2_body('') unless $extra{more};
396 }
397
398 my $frames = $s->read(all => [{ sid => $sid, fin => 1 }]);
399
400 my ($frame) = grep { $_->{type} eq "HEADERS" } @$frames;
401 my ($data) = grep { $_->{type} eq "DATA" } @$frames;
402
403 return join("\n", map { "$_: " . $frame->{headers}->{$_}; }
404 keys %{$frame->{headers}}) . "\n\n" . $data->{data};
405 }
406
407 ###############################################################################