Mercurial > hg > nginx-tests
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 ############################################################################### |