[PATCH] Tests: HTTP/2 tests with error_page and return

Sergey Kandaurov pluknet at nginx.com
Thu May 11 11:31:25 UTC 2023


> On 3 May 2023, at 01:45, Maxim Dounin <mdounin at mdounin.ru> wrote:
> 
> Hello!
> 
> On Tue, May 02, 2023 at 05:10:55PM +0400, Sergey Kandaurov wrote:
> 
>>> On 2 May 2023, at 00:59, Maxim Dounin <mdounin at mdounin.ru> wrote:
>>> 
>>> On Mon, May 01, 2023 at 06:46:25PM +0400, Sergey Kandaurov wrote:
>>> 
>>>> # HG changeset patch
>>>> # User Sergey Kandaurov <pluknet at nginx.com>
>>>> # Date 1682952238 -14400
>>>> #      Mon May 01 18:43:58 2023 +0400
>>>> # Node ID 90aaa942972884dcd67b6744fde39a154fec5d13
>>>> # Parent  36a4563f7f005184547575f5ac4f22ef53a59c72
>>>> Tests: HTTP/2 tests with error_page and return.
>>>> 
>>>> diff --git a/h2_error_page.t b/h2_error_page.t
>>>> new file mode 100644
>>>> --- /dev/null
>>>> +++ b/h2_error_page.t
>>>> @@ -0,0 +1,88 @@
>>>> +#!/usr/bin/perl
>>>> +
>>>> +# (C) Sergey Kandaurov
>>>> +# (C) Nginx, Inc.
>>>> +
>>>> +# Tests for HTTP/2 protocol with error_page directive.
>>>> +
>>>> +###############################################################################
>>>> +
>>>> +use warnings;
>>>> +use strict;
>>>> +
>>>> +use Test::More;
>>>> +
>>>> +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 rewrite/)->plan(2)
>>>> +	->write_file_expand('nginx.conf', <<'EOF');
>>>> +
>>>> +%%TEST_GLOBALS%%
>>>> +
>>>> +daemon off;
>>>> +
>>>> +events {
>>>> +}
>>>> +
>>>> +http {
>>>> +    %%TEST_GLOBALS_HTTP%%
>>>> +
>>>> +    server {
>>>> +        listen       127.0.0.1:8080 http2;
>>>> +        server_name  localhost;
>>>> +
>>>> +        lingering_close off;
>>>> +
>>>> +        error_page 400 = /close;
>>>> +
>>>> +        location / { }
>>>> +
>>>> +        location /close {
>>>> +            return 444;
>>>> +        }
>>>> +    }
>>>> +}
>>>> +
>>>> +EOF
>>>> +
>>>> +$t->run();
>>>> +
>>>> +###############################################################################
>>>> +
>>>> +my ($sid, $frames, $frame);
>>>> +
>>>> +# tests for socket leak with "return 444" in error_page
>>>> +
>>>> +# ticket #274
>>>> +
>>>> +my $s1 = Test::Nginx::HTTP2->new();
>>>> +$sid = $s1->new_stream({ headers => [
>>>> +        { name => ':method', value => 'GET' },
>>>> +        { name => ':path', value => '/' },
>>>> +        { name => ':authority', value => 'localhost' }]});
>>>> +$frames = $s1->read(all => [{ type => 'RST_STREAM' }]);
>>>> +
>>>> +($frame) = grep { $_->{type} eq "RST_STREAM" } @$frames;
>>>> +is($frame->{sid}, $sid, 'error 400 return 444 - missing header');
>>> 
>>> This clearly needs details about the header being missed, as well 
>>> as expected and observed behaviour, not just the ticket number.
>> 
>> The description is provided in associated commit logs, a proper
>> source to seek for details, tagged with appropriate ticket numbers.
>> A brief description what happens here is given above.
> 
> Even assuming commits are readily available (they are not in 
> most cases), commit logs and even the code changes are not enough 
> to see what actually missed here: that is, it worth to mention 
> lack of mandatory ":scheme" pseudo-header.

Sure, I don't mind to add extra comments
if that provides further explanation.

> 
> Also, it might be important to mention why the test is expected to 
> fail without the fix (and if it's expected to fail), and why it 
> succeeds with the fix.  Note that the tickets in question are 
> about connection being left open, and not about RST_STREAM not 
> being sent.

Well, the connection is expected to be kept.
Unlike in HTTP/1.x, HTTP/2 is a multiplexing protocol.
That means an error condition in an individual stream doesn't lead
to the entire connection close.  Rather, malformed requests lead
to a stream error indicated with RST_STREAM.

Let's look at RFC 7540:

5.4.  Error Handling
   HTTP/2 framing permits two classes of error:
   o  An error condition that renders the entire connection unusable is
      a connection error.
   o  An error in an individual stream is a stream error.

So either GOAWAY or RST_STREAM is expected.

8.1.2.6.  Malformed Requests and Responses
   Malformed requests or responses that are
   detected MUST be treated as a stream error (Section 5.4.2) of type
   PROTOCOL_ERROR.

This is exactly what happens in nginx: it sends RST_STREAM
as an indication of immediate termination of a stream.

> 
> Note well that RST_STREAM is not something one might expect with 
> "return 444;", and rather an implementation detail.

See above, thanks.

>  A better 
> approach might be to check instead for a connection being closed 
> and not timed out on the client side (this might complicate things 
> though, and might not worth the effort).

The problem is that stream termination didn't happen.
As such, stream count never fall below 1, which prevented
to install a keepalive timer.  So, when it's time to shutdown
a worker process, there are no timers left in connection.

> 
>> If you insist, we can add further details inline:
>> 
>> # a socket leak observed on missing ngx_http_run_posted_requests() such as
>> # in ngx_http_v2_run_request(), e.g. if ngx_http_v2_construct_request_line()
>> # failed due to missing mandatory ":scheme" pseudo-header (ticket #274)
> 
> These are indeed mostly implementation details.  A better comment 
> might be:
> 
> # make sure there is no socket leak when the request is rejected
> # due to missing mandatory ":scheme" pseudo-header and "return 444;"
> # is used in error_page 400 (ticket #274)
> 
>>> 
>>>> +
>>>> +# ticket #2455
>>>> +
>>>> +my $s2 = Test::Nginx::HTTP2->new();
>>>> +$sid = $s2->new_stream({ method => 'foo' });
>>>> +$frames = $s2->read(all => [{ type => 'RST_STREAM' }]);
>>>> +
>>>> +($frame) = grep { $_->{type} eq "RST_STREAM" } @$frames;
>>>> +is($frame->{sid}, $sid, 'error 400 return 444 - invalid header');
>>> 
>>> Same here.
>> 
>> # another case with missing ngx_http_run_posted_requests() is in
>> # ngx_http_v2_state_process_header() error handling (ticket #2455),
>> # can be triggered with invalid pseudo-header
> 
> Same here:
> 
> # make sure there is no socket leak when the request is rejected
> # due to invalid method with lower-case letters and "return 444;"
> # is used in error_page 400 (ticket #2455)
> 

Thank you, much appreciated.
Applied to the changeset.

>> 
>>> 
>>>> +
>>>> +$t->stop();
> 
> This also might worth an explicit comment.  Something like
> 
> # while keeping $s1 and $s2, stop nginx; this should result in
> # "open socket ... left in connection ..." alerts if any of these
> # sockets is still open
> 
> should be good enough.
> 

Thanks for the review, pushed.

-- 
Sergey Kandaurov


More information about the nginx-devel mailing list