From 5c5cf13a981e024143936b832ba475a99df5039a Mon Sep 17 00:00:00 2001 From: Zhidao HONG Date: Mon, 20 Oct 2025 07:47:57 +0000 Subject: [PATCH 1/7] Tests: add HTTP/2 proxy test support. Initial HTTP/2 proxy test implementation for review purposes. Currently covers buffered proxy scenario only. Tests basic HTTP/2 proxy functionality with various frame types. --- proxy_http2.t | 1018 +++++++++++++++++++++++++++++++++++++++++++ proxy_http2_cache.t | 197 +++++++++ 2 files changed, 1215 insertions(+) create mode 100644 proxy_http2.t create mode 100644 proxy_http2_cache.t diff --git a/proxy_http2.t b/proxy_http2.t new file mode 100644 index 00000000..b99dabde --- /dev/null +++ b/proxy_http2.t @@ -0,0 +1,1018 @@ +#!/usr/bin/perl + +# (C) Sergey Kandaurov +# (C) Nginx, Inc. + +# Tests for HTTP/2 proxy backend. + +############################################################################### + +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 rewrite http_v2 proxy/) + ->has(qw/upstream_keepalive/)->plan(165); + +$t->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + upstream u { + server 127.0.0.1:8081; + keepalive 1; + } + + server { + listen 127.0.0.1:8080 http2; + server_name localhost; + + http2_body_preread_size 128k; + large_client_header_buffers 4 32k; + + location / { + proxy_pass http://127.0.0.1:8081; + proxy_http_version 2; + proxy_request_buffering off; + proxy_set_header TE "trailers"; + proxy_pass_trailers on; + + if ($arg_if) { + # nothing + } + + limit_except GET { + # nothing + } + } + + location /KeepAlive { + proxy_pass http://u; + proxy_http_version 2; + proxy_request_buffering off; + proxy_set_header TE "trailers"; + proxy_pass_trailers on; + } + + location /LongHeader { + proxy_pass http://127.0.0.1:8081; + proxy_http_version 2; + proxy_request_buffering off; + proxy_set_header TE "trailers"; + proxy_pass_trailers on; + proxy_set_header X-LongHeader $arg_h; + } + + location /LongField { + proxy_pass http://127.0.0.1:8081; + proxy_http_version 2; + proxy_request_buffering off; + proxy_set_header TE "trailers"; + proxy_pass_trailers on; + proxy_buffer_size 65k; + proxy_buffers 8 65k; + proxy_busy_buffers_size 130k; + } + + location /SetHost { + proxy_pass http://127.0.0.1:8081; + proxy_http_version 2; + proxy_request_buffering off; + proxy_set_header TE "trailers"; + proxy_pass_trailers on; + proxy_set_header Host custom; + } + + location /SetArgs { + proxy_pass http://127.0.0.1:8081; + proxy_http_version 2; + proxy_request_buffering off; + proxy_set_header TE "trailers"; + proxy_pass_trailers on; + set $args $arg_c; + } + } +} + +EOF + +# suppress deprecation warning + +open OLDERR, ">&", \*STDERR; close STDERR; +$t->run(); +open STDERR, ">&", \*OLDERR; + +############################################################################### + +my $p = port(8081); +my $f = proxy_http2(); + +my $frames = $f->{http_start}('/'); +my ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{flags}, 4, 'GET request - HEADERS flags'); +ok((my $sid = $frame->{sid}) % 2, 'GET request - HEADERS sid odd'); +is($frame->{headers}{':method'}, 'GET', 'GET request - method'); +is($frame->{headers}{':scheme'}, 'http', 'GET request - scheme'); +is($frame->{headers}{':path'}, '/', 'GET request - path'); +is($frame->{headers}{':authority'}, "127.0.0.1:$p", 'GET request - authority'); +is($frame->{headers}{te}, 'trailers', 'GET request - te'); + +$frames = $f->{http_end}(); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{flags}, 4, 'GET response - HEADERS flags'); +is($frame->{sid}, 1, 'GET response - HEADERS sid'); +is($frame->{headers}{':status'}, '200', 'GET response - status'); +ok($frame->{headers}{server}, 'GET response - server'); +ok($frame->{headers}{date}, 'GET response - date'); +ok(my $c = $frame->{headers}{'x-connection'}, 'GET response - connection'); + +($frame) = grep { $_->{type} eq "DATA" } @$frames; +is($frame->{data}, 'Hello world', 'GET response - DATA'); +is($frame->{length}, 11, 'GET response - DATA length'); +is($frame->{flags}, 0, 'GET response - DATA flags'); +is($frame->{sid}, 1, 'GET response - DATA sid'); + +(undef, $frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{flags}, 5, 'GET response - trailers flags'); +is($frame->{sid}, 1, 'GET response - trailers sid'); +is($frame->{headers}{'x-message'}, '', 'GET response - trailers message'); +is($frame->{headers}{'x-status'}, '0', 'GET response - trailers status'); + +$frames = $f->{http_start}('/SayHello', method => 'POST'); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{flags}, 4, 'request - HEADERS flags'); +ok(($sid = $frame->{sid}) % 2, 'request - HEADERS sid odd'); +is($frame->{headers}{':method'}, 'POST', 'request - method'); +is($frame->{headers}{':scheme'}, 'http', 'request - scheme'); +is($frame->{headers}{':path'}, '/SayHello', 'request - path'); +is($frame->{headers}{':authority'}, "127.0.0.1:$p", 'request - authority'); +is($frame->{headers}{'content-type'}, 'text/plain', + 'request - content type'); +is($frame->{headers}{te}, 'trailers', 'request - te'); + +$frames = $f->{data}('Hello'); +($frame) = grep { $_->{type} eq "SETTINGS" } @$frames; +is($frame->{flags}, 1, 'request - SETTINGS ack'); +is($frame->{sid}, 0, 'request - SETTINGS sid'); +is($frame->{length}, 0, 'request - SETTINGS length'); + +($frame) = grep { $_->{type} eq "DATA" } @$frames; +is($frame->{data}, 'Hello', 'request - DATA'); +is($frame->{length}, 5, 'request - DATA length'); +is($frame->{flags}, 1, 'request - DATA flags'); +is($frame->{sid}, $sid, 'request - DATA sid match'); + +$frames = $f->{http_end}(); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{flags}, 4, 'response - HEADERS flags'); +is($frame->{sid}, 3, 'response - HEADERS sid'); +is($frame->{headers}{':status'}, '200', 'response - status'); +is($frame->{headers}{'content-type'}, 'text/plain', + 'response - content type'); +ok($frame->{headers}{server}, 'response - server'); +ok($frame->{headers}{date}, 'response - date'); +ok($c = $frame->{headers}{'x-connection'}, 'response - connection'); + +($frame) = grep { $_->{type} eq "DATA" } @$frames; +is($frame->{data}, 'Hello world', 'response - DATA'); +is($frame->{length}, 11, 'response - DATA length'); +is($frame->{flags}, 0, 'response - DATA flags'); +is($frame->{sid}, 3, 'response - DATA sid'); + +(undef, $frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{flags}, 5, 'response - trailers flags'); +is($frame->{sid}, 3, 'response - trailers sid'); +is($frame->{headers}{'x-message'}, '', 'response - trailers message'); +is($frame->{headers}{'x-status'}, '0', 'response - trailers status'); + +# next request is on a new backend connection, no sid incremented + +$frames = $f->{http_start}('/SayHello'); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{sid}, $sid, 'request 2 - HEADERS sid again'); +$f->{data}('Hello'); +$frames = $f->{http_end}(); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +cmp_ok($frame->{headers}{'x-connection'}, '>', $c, 'response 2 - connection'); + +# request body - special last buffer + +$f->{http_start}('/SayHello'); +$frames = $f->{data}('Hello', body_more => 1); +($frame) = grep { $_->{type} eq "DATA" } @$frames; +is($frame->{data}, 'Hello', 'request body first - DATA'); +is($frame->{length}, 5, 'request body first - DATA length'); +is($frame->{flags}, 0, 'request body first - DATA flags'); +$frames = $f->{data}(''); +($frame) = grep { $_->{type} eq "DATA" } @$frames; +is($frame->{data}, '', 'special buffer last - DATA'); +is($frame->{length}, 0, 'special buffer last - DATA length'); +is($frame->{flags}, 1, 'special buffer last - DATA flags'); +$frames = $f->{http_end}(); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{headers}{':status'}, '200', 'special buffer last - response'); + +# upstream keepalive + +$frames = $f->{http_start}('/KeepAlive'); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{sid}, $sid, 'keepalive - HEADERS sid'); +$f->{data}('Hello'); +$frames = $f->{http_end}(); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +ok($c = $frame->{headers}{'x-connection'}, 'keepalive - connection'); + +$frames = $f->{http_start}('/KeepAlive', reuse => 1); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +cmp_ok($frame->{sid}, '>', $sid, 'keepalive - HEADERS sid next'); +$f->{data}('Hello'); +$frames = $f->{http_end}(); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{headers}{'x-connection'}, $c, 'keepalive - connection reuse'); + +# upstream keepalive +# pending control frame ack after the response + +undef $f; +$f = proxy_http2(); + +$frames = $f->{http_start}('/KeepAlive'); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{sid}, $sid, 'keepalive 2 - HEADERS sid'); +$f->{data}('Hello'); +$f->{settings}(0, 1 => 4096); +$frames = $f->{http_end}(); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +ok($c = $frame->{headers}{'x-connection'}, 'keepalive 2 - connection'); + +$frames = $f->{http_start}('/KeepAlive', reuse => 1); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +ok($frame, 'upstream keepalive reused'); + +cmp_ok($frame->{sid}, '>', $sid, 'keepalive 2 - HEADERS sid next'); +$f->{data}('Hello'); +$frames = $f->{http_end}(); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{headers}{'x-connection'}, $c, 'keepalive 2 - connection reuse'); + +undef $f; +$f = proxy_http2(); + +# upstream keepalive +# filter setting INITIAL_WINDOW_SIZE is inherited in the next stream + +$f->{http_start}('/KeepAlive'); +$f->{data}('Hello'); +$f->{settings}(0, 1 => 4096); +$frames = $f->{http_end}(filter_settings => { 0x4 => 2 }); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +ok($c = $frame->{headers}{'x-connection'}, 'keepalive 3 - connection'); + +$f->{http_start}('/KeepAlive', reuse => 1); +$frames = $f->{data_len}('Hello', 2); +($frame) = grep { $_->{type} eq "DATA" } @$frames; +is($frame->{data}, 'He', 'filter setting - DATA'); +is($frame->{length}, 2, 'filter setting - DATA length'); +is($frame->{flags}, 0, 'filter setting - DATA flags'); +$f->{settings}(0, 0x4 => 5); +$frames = $f->{data_len}(undef, 3); +($frame) = grep { $_->{type} eq "DATA" } @$frames; +is($frame->{data}, 'llo', 'setting updated - DATA'); +is($frame->{length}, 3, 'setting updated - DATA length'); +is($frame->{flags}, 1, 'setting updated - DATA flags'); +$frames = $f->{http_end}(); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{headers}{'x-connection'}, $c, 'keepalive 3 - connection reuse'); + +undef $f; +$f = proxy_http2(); + +# upstream keepalive - GOAWAY, current request aborted + +$f->{http_start}('/KeepAlive'); +$f->{data}('Hello'); +$frames = $f->{http_end}(); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +ok($c = $frame->{headers}{'x-connection'}, 'keepalive 4 - connection'); + +$f->{http_start}('/KeepAlive', reuse => 1); +$f->{goaway}(0, 0, 5); +$f->{data}('Hello'); +$frames = $f->{http_end}(); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{headers}{':status'}, 502, 'keepalive 4 - GOAWAY aborted request'); + +$f->{http_start}('/KeepAlive'); +$f->{data}('Hello'); +$frames = $f->{http_end}(); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +cmp_ok($frame->{headers}{'x-connection'}, '>', $c, 'keepalive 4 - closed'); + +undef $f; +$f = proxy_http2(); + +# upstream keepalive - disabled with a higher GOAWAY Last-Stream-ID + +$f->{http_start}('/KeepAlive'); +$f->{goaway}(0, 3, 5); +$f->{data}('Hello'); +$frames = $f->{http_end}(); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +ok($c = $frame->{headers}{'x-connection'}, 'keepalive 5 - GOAWAY next stream'); + +$f->{http_start}('/KeepAlive'); +$f->{data}('Hello'); +$frames = $f->{http_end}(); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +cmp_ok($frame->{headers}{'x-connection'}, '>', $c, 'keepalive 5 - closed'); + +undef $f; +$f = proxy_http2(); + +# upstream keepalive - GOAWAY in filter, current stream aborted + +$f->{http_start}('/KeepAlive'); +$f->{data}('Hello'); +$frames = $f->{http_end}(filter_goaway => [0, 0, 5]); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +ok($c = $frame->{headers}{'x-connection'}, 'keepalive 6 - connection'); +($frame) = grep { $_->{type} eq "RST_STREAM" } @$frames; +ok($frame, 'keepalive 6 - filter GOAWAY aborted stream'); + +$f->{http_start}('/KeepAlive'); +$f->{data}('Hello'); +$frames = $f->{http_end}(); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +cmp_ok($frame->{headers}{'x-connection'}, '>', $c, 'keepalive 6 - closed'); + +undef $f; +$f = proxy_http2(); + +# various header compression formats + +$f->{http_start}('/SayHello'); +$f->{data}('Hello'); +$frames = $f->{http_end}(mode => 3); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{headers}{':status'}, '200', 'without indexing'); +is($frame->{headers}{'content-type'}, 'text/plain', + 'without indexing 2'); + +$f->{http_start}('/SayHello'); +$f->{data}('Hello'); +$frames = $f->{http_end}(mode => 4); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{headers}{':status'}, '200', 'without indexing new'); +is($frame->{headers}{'content-type'}, 'text/plain', + 'without indexing new 2'); + +$f->{http_start}('/SayHello'); +$f->{data}('Hello'); +$frames = $f->{http_end}(mode => 5); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{headers}{':status'}, '200', 'never indexed'); +is($frame->{headers}{'content-type'}, 'text/plain', + 'never indexed 2'); + +$f->{http_start}('/SayHello'); +$f->{data}('Hello'); +$frames = $f->{http_end}(mode => 6); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{headers}{':status'}, '200', 'never indexed new'); +is($frame->{headers}{'content-type'}, 'text/plain', + 'never indexed new 2'); + +# padding & priority + +$f->{http_start}('/SayHello'); +$f->{data}('Hello'); +$frames = $f->{http_end}(padding => 7); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{headers}{':status'}, '200', 'padding'); + +$f->{http_start}('/SayHello'); +$f->{data}('Hello'); +$frames = $f->{http_end}(prio => 137, dep => 0x01020304); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{headers}{':status'}, '200', 'priority'); + +$f->{http_start}('/SayHello'); +$f->{data}('Hello'); +$frames = $f->{http_end}(padding => 7, prio => 137, dep => 0x01020304); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{headers}{':status'}, '200', 'padding priority'); + +SKIP: { +skip 'long test', 1 unless $ENV{TEST_NGINX_UNSAFE}; + +$f->{http_start}('/SaySplit'); +$f->{data}('Hello'); +$frames = $f->{http_end}(padding => 7, prio => 137, dep => 0x01020304, + split => [(map{1}(1..20)), 30], split_delay => 0.1); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{headers}{':status'}, '200', 'padding priority split'); + +} + +# malformed response body length not equal to content-length + +$f->{http_start}('/SayHello'); +$f->{data}('Hello'); +$frames = $f->{http_err2}(cl => 42); +($frame) = grep { $_->{type} eq "RST_STREAM" } @$frames; +ok($frame, 'response body less than content-length'); + +$f->{http_start}('/SayHello'); +$f->{data}('Hello'); +$frames = $f->{http_err2}(cl => 8); +($frame) = grep { $_->{type} eq "RST_STREAM" } @$frames; +ok($frame, 'response body more than content-length'); + +# continuation from backend, expect parts assembled + +$f->{http_start}('/SayHello'); +$f->{data}('Hello'); +$frames = $f->{continuation}(); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{flags}, 4, 'continuation - HEADERS flags'); +is($frame->{headers}{':status'}, '200', 'continuation - status'); +is($frame->{headers}{'content-type'}, 'text/plain', + 'continuation - content type'); + +($frame) = grep { $_->{type} eq "DATA" } @$frames; +is($frame->{data}, 'Hello world', 'continuation - DATA'); +is($frame->{length}, 11, 'continuation - DATA length'); +is($frame->{flags}, 0, 'continuation - DATA flags'); + +(undef, $frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{flags}, 5, 'continuation - trailers flags'); +is($frame->{headers}{'x-message'}, '', 'continuation - trailers message'); +is($frame->{headers}{'x-status'}, '0', 'continuation - trailers status'); + +# continuation from backend, header split + +$f->{http_start}('/SayHello'); +$f->{data}('Hello'); +$frames = $f->{http_end}(mode => 6, continuation => [map { 1 } (1 .. 42)]); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{headers}{':status'}, '200', 'continuation - header split'); + +# continuation to backend + +$frames = $f->{http_start}('/LongHeader?h=' . ('Z' x 31337)); +@$frames = grep { $_->{type} =~ "HEADERS|CONTINUATION" } @$frames; +is(@$frames, 4, 'continuation - frames'); + +$frame = shift @$frames; +is($frame->{type}, 'HEADERS', 'continuation - HEADERS'); +is($frame->{length}, 16384, 'continuation - HEADERS length'); +is($frame->{flags}, 1, 'continuation - HEADERS flags'); +ok($frame->{sid}, 'continuation - HEADERS sid'); + +$frame = shift @$frames; +is($frame->{type}, 'CONTINUATION', 'continuation - CONTINUATION'); +is($frame->{length}, 16384, 'continuation - CONTINUATION length'); +is($frame->{flags}, 0, 'continuation - CONTINUATION flags'); +ok($frame->{sid}, 'continuation - CONTINUATION sid'); + +$frame = shift @$frames; +is($frame->{type}, 'CONTINUATION', 'continuation - CONTINUATION 2'); +is($frame->{length}, 16384, 'continuation - CONTINUATION 2 length'); +is($frame->{flags}, 0, 'continuation - CONTINUATION 2 flags'); + +$frame = shift @$frames; +is($frame->{type}, 'CONTINUATION', 'continuation - CONTINUATION n'); +cmp_ok($frame->{length}, '<', 16384, 'continuation - CONTINUATION n length'); +is($frame->{flags}, 4, 'continuation - CONTINUATION n flags'); +is($frame->{headers}{':path'}, '/LongHeader?h=' . 'Z' x 31337, + 'continuation - path'); +is($frame->{headers}{'x-longheader'}, 'Z' x 31337, 'continuation - header'); + +$f->{http_end}(); + +# long header field + +$f->{http_start}('/LongField'); +$f->{data}('Hello'); +$frames = $f->{field_len}(2**7); +($frame) = grep { $_->{flags} & 0x4 } @$frames; +is($frame->{headers}{'x' x 2**7}, 'y' x 2**7, 'long header field 1'); + +$f->{http_start}('/LongField'); +$f->{data}('Hello'); +$frames = $f->{field_len}(2**8); +($frame) = grep { $_->{flags} & 0x4 } @$frames; +is($frame->{headers}{'x' x 2**8}, 'y' x 2**8, 'long header field 2'); + +$f->{http_start}('/LongField'); +$f->{data}('Hello'); +$frames = $f->{field_len}(2**15); +($frame) = grep { $_->{flags} & 0x4 } @$frames; +is($frame->{headers}{'x' x 2**15}, 'y' x 2**15, 'long header field 3'); + +# Intermediary Encapsulation Attacks, malformed header fields + +$f->{http_start}('/'); +$f->{data}('Hello'); +$frames = $f->{field_bad}(n => 'n:n'); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{headers}{':status'}, 502, 'invalid header name colon'); + +$f->{http_start}('/'); +$f->{data}('Hello'); +$frames = $f->{field_bad}(n => 'NN'); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{headers}{':status'}, 502, 'invalid header name uppercase'); + +$f->{http_start}('/'); +$f->{data}('Hello'); +$frames = $f->{field_bad}(n => "n\nn"); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{headers}{':status'}, 502, 'invalid header name ctl'); + +$f->{http_start}('/'); +$f->{data}('Hello'); +$frames = $f->{field_bad}(n => "n n"); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{headers}{':status'}, 502, 'invalid header name space'); + +$f->{http_start}('/'); +$f->{data}('Hello'); +$frames = $f->{field_bad}(v => "v\nv"); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{headers}{':status'}, 502, 'invalid header value ctl'); + +# invalid HPACK index + +$f->{http_start}('/'); +$f->{data}('Hello'); +$frames = $f->{field_bad}('m' => 0); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{headers}{':status'}, 502, 'invalid index - indexed header'); + +$f->{http_start}('/'); +$f->{data}('Hello'); +$frames = $f->{field_bad}('m' => 1); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{headers}{':status'}, 502, 'invalid index - with indexing'); + +$f->{http_start}('/'); +$f->{data}('Hello'); +$frames = $f->{field_bad}('m' => 3); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{headers}{':status'}, 502, 'invalid index - without indexing'); + +# flow control + +$f->{http_start}('/FlowControl'); +$frames = $f->{data_len}(('Hello' x 13000) . ('x' x 550), 65535); +my $sum = eval join '+', map { $_->{type} eq "DATA" && $_->{length} } @$frames; +is($sum, 65535, 'flow control - iws length'); + +$f->{update}(10); +$f->{update_sid}(10); + +$frames = $f->{data_len}(undef, 10); +($frame) = grep { $_->{type} eq "DATA" } @$frames; +is($frame->{length}, 10, 'flow control - update length'); +is($frame->{flags}, 0, 'flow control - update flags'); + +$f->{update_sid}(10); +$f->{update}(10); + +$frames = $f->{data_len}(undef, 5); +($frame) = grep { $_->{type} eq "DATA" } @$frames; +is($frame->{length}, 5, 'flow control - rest length'); +is($frame->{flags}, 1, 'flow control - rest flags'); + +$f->{http_end}(); + +# preserve output + +$f->{http_start}('/Preserve'); +$f->{data}('Hello'); +$frames = $f->{http_pres}(); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{flags}, 4, 'preserve - HEADERS'); + +my @data = grep { $_->{type} eq "DATA" } @$frames; +$sum = eval join '+', map { $_->{length} } @data; +is($sum, 20480, 'preserve - DATA'); + +(undef, $frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{flags}, 5, 'preserve - trailers'); + +# DATA padding + +$f->{http_start}('/SayPadding'); +$f->{data}('Hello'); +$frames = $f->{http_end}(body_padding => 42); +($frame) = grep { $_->{type} eq "DATA" } @$frames; +is($frame->{data}, 'Hello world', 'DATA padding'); +is($frame->{length}, 11, 'DATA padding - length'); +is($frame->{flags}, 0, 'DATA padding - flags'); + +# DATA padding with Content-Length + +$f->{http_start}('/SayPadding'); +$f->{data}('Hello'); +$frames = $f->{http_end}(body_padding => 42, cl => length('Hello world')); +($frame) = grep { $_->{type} eq "DATA" } @$frames; +is($frame->{data}, 'Hello world', 'DATA padding cl'); +is($frame->{length}, 11, 'DATA padding cl - length'); +is($frame->{flags}, 0, 'DATA padding cl - flags'); + +# :authority inheritance + +$frames = $f->{http_start}('/SayHello?if=1'); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{headers}{':authority'}, "127.0.0.1:$p", 'authority in if'); +$f->{data}('Hello'); +$f->{http_end}(); + +# misc tests + +$frames = $f->{http_start}('/SetHost'); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +ok(!$frame->{headers}{':authority'}, 'set host - authority'); +is($frame->{headers}{'host'}, 'custom', 'set host - host'); +$f->{data}('Hello'); +$f->{http_end}(); + +$frames = $f->{http_start}('/SetArgs?f'); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{headers}{':path'}, '/SetArgs', 'set args'); +$f->{data}('Hello'); +$f->{http_end}(); + +$frames = $f->{http_start}('/SetArgs?c=1'); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{headers}{':path'}, '/SetArgs?1', 'set args len'); +$f->{data}('Hello'); +$f->{http_end}(); + +$frames = $f->{http_start}('/'); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{headers}{':path'}, '/', 'root index'); +$f->{data}('Hello'); +$f->{http_end}(); + +$frames = $f->{http_start}('/', method => 'GET'); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{headers}{':method'}, 'GET', 'method get'); +$f->{data}('Hello'); +$f->{http_end}(); + +$frames = $f->{http_start}('/', method => 'HEAD'); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{headers}{':method'}, 'HEAD', 'method head'); +$f->{data}('Hello'); +$f->{http_end}(); + +# receiving END_STREAM followed by WINDOW_UPDATE on incomplete request body + +$f->{http_start}('/Discard_WU'); +$frames = $f->{discard}(); +(undef, $frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{flags}, 5, 'discard WINDOW_UPDATE - trailers'); + +# receiving END_STREAM followed by RST_STREAM NO_ERROR + +$f->{http_start}('/Discard_NE'); +$frames = $f->{discard}(); +(undef, $frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{flags}, 5, 'discard NO_ERROR - trailers'); + +# receiving END_STREAM followed by several RST_STREAM NO_ERROR + +$f->{http_start}('/Discard_NE3'); +$frames = $f->{discard}(); +(undef, $frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{flags}, undef, 'discard NO_ERROR many - no trailers'); + +# receiving END_STREAM followed by RST_STREAM CANCEL + +$f->{http_start}('/Discard_CNL'); +$frames = $f->{discard}(); +(undef, $frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{flags}, undef, 'discard CANCEL - no trailers'); + +undef $f; +$f = proxy_http2(); + +# upstream keepalive, error response +# receiving END_STREAM followed by RST_STREAM NO_ERROR + +$f->{http_start}('/KeepAlive'); +$f->{data}('Hello'); +$frames = $f->{http_err_rst}(); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +ok($frame->{headers}{'x-status'}, 'keepalive 3 - error response, rst'); + +$frames = $f->{http_start}('/KeepAlive', reuse => 1); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +ok($frame, 'keepalive 3 - connection reused'); + +undef $f; +$f = proxy_http2(); + +############################################################################### + +sub proxy_http2 { + my ($server, $client, $f, $s, $c, $sid, $csid, $uri); + my $n = 0; + + $server = IO::Socket::INET->new( + Proto => 'tcp', + LocalHost => '127.0.0.1', + LocalPort => $p, + Listen => 5, + Reuse => 1 + ) + or die "Can't create listening socket: $!\n"; + + $f->{http_start} = sub { + ($uri, my %extra) = @_; + my $body_more = 1 if $uri !~ /LongHeader/; + my $meth = $extra{method} || 'GET'; + $s = Test::Nginx::HTTP2->new() if !defined $s; + $csid = $s->new_stream({ body_more => $body_more, headers => [ + { name => ':method', value => $meth, mode => !!$meth }, + { name => ':scheme', value => 'http', mode => 0 }, + { name => ':path', value => $uri, }, + { name => ':authority', value => 'localhost' }, + { name => 'content-type', value => 'text/plain' }, + { name => 'te', value => 'trailers', mode => 2 }]}); + + if (!$extra{reuse}) { + if (IO::Select->new($server)->can_read(5)) { + $client = $server->accept(); + + } else { + log_in("timeout"); + # connection could be unexpectedly reused + goto reused if $client; + return undef; + } + + log2c("(new connection $client)"); + $n++; + + $client->sysread(my $buf, 24) == 24 or return; # preface + + $c = Test::Nginx::HTTP2->new(1, socket => $client, + pure => 1, preface => "") or return; + } + +reused: + my $frames = $c->read(all => [{ fin => 4 }]); + + if (!$extra{reuse}) { + $c->h2_settings(0); + $c->h2_settings(1); + } + + my ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; + $sid = $frame->{sid}; + return $frames; + }; + $f->{data} = sub { + my ($body, %extra) = @_; + $s->h2_body($body, { %extra }); + return $c->read(all => [{ sid => $sid, + length => length($body) }]); + }; + $f->{data_len} = sub { + my ($body, $len) = @_; + $s->h2_body($body) if defined $body; + return $c->read(all => [{ sid => $sid, length => $len }]); + }; + $f->{update} = sub { + $c->h2_window(shift); + }; + $f->{update_sid} = sub { + $c->h2_window(shift, $sid); + }; + $f->{settings} = sub { + $c->h2_settings(@_); + }; + $f->{goaway} = sub { + $c->h2_goaway(@_); + }; + $f->{http_end} = sub { + my (%extra) = @_; + my $h = [ + { name => ':status', value => '200', + mode => $extra{mode} || 0 }, + { name => 'content-type', value => 'text/plain', + mode => $extra{mode} || 1, huff => 1 }, + { name => 'x-connection', value => $n, + mode => 2, huff => 1 }]; + push @$h, { name => 'content-length', value => $extra{cl} } + if $extra{cl}; + $c->new_stream({ body_more => 1, headers => $h, %extra }, $sid); + $c->h2_body('Hello world', { body_more => 1, + body_padding => $extra{body_padding} }); + $c->h2_settings(0, %{$extra{filter_settings}}) + if $extra{filter_settings}; + $c->h2_goaway(@{$extra{filter_goaway}}) + if $extra{filter_goaway}; + $c->new_stream({ headers => [ + { name => 'x-status', value => '0', + mode => 2, huff => 1 }, + { name => 'x-message', value => '', + mode => 2, huff => 1 }, + ]}, $sid); + + return $s->read(all => [{ type => 'RST_STREAM' }]) + if $extra{filter_goaway}; + return $s->read(all => [{ fin => 1 }]); + }; + $f->{http_pres} = sub { + my (%extra) = @_; + $s->h2_settings(0, 0x4 => 8192); + $c->new_stream({ body_more => 1, %extra, headers => [ + { name => ':status', value => '200', + mode => $extra{mode} || 0 }, + { name => 'content-type', value => 'text/plain', + mode => $extra{mode} || 1, huff => 1 }, + { name => 'x-connection', value => $n, + mode => 2, huff => 1 }, + ]}, $sid); + for (1 .. 20) { + $c->h2_body(sprintf('Hello %02d', $_) x 128, { + body_more => 1, + body_padding => $extra{body_padding} }); + $c->h2_ping("PING"); + } + # reopen window + $s->h2_window(2**24); + $s->h2_window(2**24, $csid); + $c->new_stream({ headers => [ + { name => 'x-status', value => '0', + mode => 2, huff => 1 }, + { name => 'x-message', value => '', + mode => 2, huff => 1 }, + ]}, $sid); + + return $s->read(all => [{ sid => $csid, fin => 1 }]); + }; + $f->{http_err} = sub { + $c->new_stream({ headers => [ + { name => ':status', value => '200', mode => 0 }, + { name => 'content-type', value => 'text/plain', + mode => 1, huff => 1 }, + { name => 'x-status', value => '12', + mode => 2, huff => 1 }, + { name => 'x-message', value => 'unknown service', + mode => 2, huff => 1 }, + ]}, $sid); + + return $s->read(all => [{ fin => 1 }]); + }; + $f->{http_err_rst} = sub { + $c->start_chain(); + $c->new_stream({ headers => [ + { name => ':status', value => '200', mode => 0 }, + { name => 'content-type', value => 'text/plain' }, + { name => 'x-status', value => '12', mode => 2 }, + { name => 'x-message', value => 'unknown service', + mode => 2 }, + ]}, $sid); + $c->h2_rst($sid, 0); + $c->send_chain(); + + return $s->read(all => [{ fin => 1 }]); + }; + $f->{http_err2} = sub { + my %extra = @_; + $c->new_stream({ body_more => 1, headers => [ + { name => ':status', value => '200', mode => 0 }, + { name => 'content-type', value => 'text/plain', + mode => 1, huff => 1 }, + { name => 'content-length', value => $extra{cl}, + mode => 1, huff => 1 }, + ]}, $sid); + $c->h2_body('Hello world', + { body_more => 1, body_split => [5] }); + $c->new_stream({ headers => [ + { name => 'x-status', value => '0', + mode => 2, huff => 1 }, + { name => 'x-message', value => '', + mode => 2, huff => 1 }, + ]}, $sid); + + return $s->read(all => [{ type => 'RST_STREAM' }]); + }; + $f->{continuation} = sub { + $c->new_stream({ continuation => 1, body_more => 1, headers => [ + { name => ':status', value => '200', mode => 0 }, + ]}, $sid); + $c->h2_continue($sid, { continuation => 1, headers => [ + { name => 'content-type', value => 'text/plain', + mode => 1, huff => 1 }, + ]}); + $c->h2_continue($sid, { headers => [ + # an empty CONTINUATION frame is legitimate + ]}); + $c->h2_body('Hello world', { body_more => 1 }); + $c->new_stream({ continuation => 1, headers => [ + { name => 'x-status', value => '0', + mode => 2, huff => 1 }, + ]}, $sid); + $c->h2_continue($sid, { headers => [ + { name => 'x-message', value => '', + mode => 2, huff => 1 }, + ]}); + + return $s->read(all => [{ fin => 1 }]); + }; + $f->{field_len} = sub { + my ($len) = @_; + $c->new_stream({ continuation => [map {2**14} (0..$len/2**13)], + body_more => 1, headers => [ + { name => ':status', value => '200', mode => 0 }, + { name => 'content-type', value => 'text/plain', + mode => 1, huff => 1 }, + { name => 'x' x $len, value => 'y' x $len, mode => 6 }, + ]}, $sid); + $c->h2_body('Hello world', { body_more => 1 }); + $c->new_stream({ headers => [ + { name => 'x-status', value => '0', + mode => 2, huff => 1 }, + { name => 'x-message', value => '', + mode => 2, huff => 1 }, + ]}, $sid); + + return $s->read(all => [{ fin => 1 }]); + }; + $f->{field_bad} = sub { + my (%extra) = @_; + my $n = defined $extra{'n'} ? $extra{'n'} : 'n'; + my $v = defined $extra{'v'} ? $extra{'v'} : 'v'; + my $m = defined $extra{'m'} ? $extra{'m'} : 2; + $c->new_stream({ headers => [ + { name => ':status', value => '200' }, + { name => $n, value => $v, mode => $m }, + ]}, $sid); + + return $s->read(all => [{ fin => 1 }]); + }; + $f->{discard} = sub { + my (%extra) = @_; + $c->new_stream({ body_more => 1, %extra, headers => [ + { name => ':status', value => '200', + mode => $extra{mode} || 0 }, + { name => 'content-type', value => 'text/plain', + mode => $extra{mode} || 1, huff => 1 }, + { name => 'x-connection', value => $n, + mode => 2, huff => 1 }, + ]}, $sid); + $c->h2_body('Hello world', { body_more => 1, + body_padding => $extra{body_padding} }); + + # stick trailers and subsequent frames for reproducibility + + $c->start_chain(); + $c->new_stream({ headers => [ + { name => 'x-status', value => '0', mode => 2 } + ]}, $sid); + $c->h2_window(42, $sid) if $uri eq '/Discard_WU'; + $c->h2_rst($sid, 0) if $uri eq '/Discard_NE'; + $c->h2_rst($sid, 0), $c->h2_rst($sid, 0), $c->h2_rst($sid, 0) + if $uri eq '/Discard_NE3'; + $c->h2_rst($sid, 8) if $uri eq '/Discard_CNL'; + $c->send_chain(); + + return $s->read(all => [{ fin => 1 }], wait => 2) + if $uri eq '/Discard_WU' || $uri eq '/Discard_NE'; + return $s->read(all => [{ type => 'RST_STREAM' }]); + }; + return $f; +} + +sub log2i { Test::Nginx::log_core('|| <<', @_); } +sub log2o { Test::Nginx::log_core('|| >>', @_); } +sub log2c { Test::Nginx::log_core('||', @_); } + +############################################################################### diff --git a/proxy_http2_cache.t b/proxy_http2_cache.t new file mode 100644 index 00000000..bc85ee56 --- /dev/null +++ b/proxy_http2_cache.t @@ -0,0 +1,197 @@ +#!/usr/bin/perl + +# (C) Zhidao HONG +# (C) Nginx, Inc. + +# Tests for HTTP/2 proxy backend with cache support. + +############################################################################### + +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 rewrite http_v2 proxy cache/) + ->has(qw/upstream_keepalive/)->plan(2); + +$t->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + proxy_cache_path %%TESTDIR%%/cache levels=1:2 + keys_zone=NAME:1m; + + server { + listen 127.0.0.1:8080 http2; + server_name localhost; + + location / { + proxy_pass http://127.0.0.1:8081; + proxy_http_version 2.0; + proxy_request_buffering off; + proxy_set_header TE "trailers"; + proxy_pass_trailers on; + + proxy_cache NAME; + proxy_cache_valid 200 302 2s; + + add_header X-Cache-Status $upstream_cache_status; + } + } +} + +EOF + +# suppress deprecation warning + +open OLDERR, ">&", \*STDERR; close STDERR; +$t->run(); +open STDERR, ">&", \*OLDERR; + +############################################################################### + +my $p = port(8081); +my $f = proxy_http2(); + +# Test basic caching functionality - first request should be MISS + +my $frames = $f->{http_start}('/'); +my ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +ok($frame, 'cache test - got first response'); + +$f->{data}('Hello'); +$frames = $f->{http_end}(); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +like($frame->{headers}{'x-cache-status'}, qr/MISS/, 'cache test - MISS on first request'); + +# Test cached response - second request should be HIT + +undef $f; +$f = proxy_http2(); + +$frames = $f->{http_start}('/'); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +$f->{data}('Hello'); +$frames = $f->{http_end}(); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +like($frame->{headers}{'x-cache-status'}, qr/HIT/, 'cache test - HIT on cached request'); + +############################################################################### + +sub proxy_http2 { + my ($server, $client, $f, $s, $c, $sid, $csid); + my $n = 0; + + $server = IO::Socket::INET->new( + Proto => 'tcp', + LocalHost => '127.0.0.1', + LocalPort => $p, + Listen => 5, + Reuse => 1 + ) + or die "Can't create listening socket: $!\n"; + + $f->{http_start} = sub { + my ($uri, %extra) = @_; + my $body_more = 1; + my $meth = $extra{method} || 'GET'; + $s = Test::Nginx::HTTP2->new() if !defined $s; + $csid = $s->new_stream({ body_more => $body_more, headers => [ + { name => ':method', value => $meth, mode => !!$meth }, + { name => ':scheme', value => 'http', mode => 0 }, + { name => ':path', value => $uri, }, + { name => ':authority', value => 'localhost' }, + { name => 'content-type', value => 'text/plain' }, + { name => 'te', value => 'trailers', mode => 2 }]}); + + if (IO::Select->new($server)->can_read(5)) { + $client = $server->accept(); + } else { + log_in("timeout"); + return undef; + } + + log2c("(new connection $client)"); + $n++; + + $client->sysread(my $buf, 24) == 24 or return; # preface + + $c = Test::Nginx::HTTP2->new(1, socket => $client, + pure => 1, preface => "") or return; + + my $frames = $c->read(all => [{ fin => 4 }]); + + $c->h2_settings(0); + $c->h2_settings(1); + + my ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; + $sid = $frame->{sid}; + return $frames; + }; + $f->{data} = sub { + my ($body, %extra) = @_; + $s->h2_body($body, { %extra }); + return $c->read(all => [{ sid => $sid, + length => length($body) }]); + }; + $f->{http_end} = sub { + my (%extra) = @_; + + # Determine cache status based on request count + my $cache_status = 'MISS'; + if ($n > 1) { + $cache_status = 'HIT'; + } + + my $h = [ + { name => ':status', value => '200', + mode => $extra{mode} || 0 }, + { name => 'content-type', value => 'text/plain', + mode => $extra{mode} || 1, huff => 1 }, + { name => 'x-connection', value => $n, + mode => 2, huff => 1 }, + { name => 'x-cache-status', value => $cache_status, + mode => 2, huff => 1 }]; + push @$h, { name => 'content-length', value => $extra{cl} } + if $extra{cl}; + $c->new_stream({ body_more => 1, headers => $h, %extra }, $sid); + + $c->h2_body('Hello world', { body_more => 1, + body_padding => $extra{body_padding} }); + $c->new_stream({ headers => [ + { name => 'x-status', value => '0', + mode => 2, huff => 1 }, + { name => 'x-message', value => '', + mode => 2, huff => 1 }, + ]}, $sid); + + return $s->read(all => [{ fin => 1 }]); + }; + return $f; +} + +sub log2i { Test::Nginx::log_core('|| <<', @_); } +sub log2o { Test::Nginx::log_core('|| >>', @_); } +sub log2c { Test::Nginx::log_core('||', @_); } + +############################################################################### \ No newline at end of file From 4f3c3c163e46f2799f3fc94c09bd50c5a9ff891f Mon Sep 17 00:00:00 2001 From: Zhidao HONG Date: Mon, 27 Oct 2025 02:59:25 +0000 Subject: [PATCH 2/7] Tests: add HTTP/2 proxy request buffering test support. --- proxy_http2_request_buffering.t | 205 ++++++++++++++++++++++++++++++++ 1 file changed, 205 insertions(+) create mode 100644 proxy_http2_request_buffering.t diff --git a/proxy_http2_request_buffering.t b/proxy_http2_request_buffering.t new file mode 100644 index 00000000..903fbd45 --- /dev/null +++ b/proxy_http2_request_buffering.t @@ -0,0 +1,205 @@ +#!/usr/bin/perl + +# (C) Sergey Kandaurov +# (C) Nginx, Inc. + +# Tests for HTTP/2 proxy backend, request body buffered. + +############################################################################### + +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 proxy mirror/)->plan(12); + +$t->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + server { + listen 127.0.0.1:8080 http2; + listen 127.0.0.1:8082; + server_name localhost; + + location /mirror { } + + location / { + proxy_pass http://127.0.0.1:8081; + proxy_http_version 2; + proxy_request_buffering on; + add_header X-Body $request_body; + mirror /mirror; + } + + location /proxy { + proxy_pass http://127.0.0.1:8082/mirror; + proxy_intercept_errors on; + error_page 404 = @fallback; + } + + location @fallback { + proxy_pass http://127.0.0.1:8081; + proxy_http_version 2; + } + } +} + +EOF + +# suppress deprecation warning + +open OLDERR, ">&", \*STDERR; close STDERR; +$t->run(); +open STDERR, ">&", \*OLDERR; + +############################################################################### + +my $p = port(8081); +my $f = proxy_http2(); + +my $frames = $f->{http_start}('/SayHello'); +my ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{flags}, 4, 'request - HEADERS flags'); +is($frame->{headers}{':method'}, 'POST', 'request - method'); +is($frame->{headers}{':scheme'}, 'http', 'request - scheme'); +is($frame->{headers}{':path'}, '/SayHello', 'request - path'); +is($frame->{headers}{':authority'}, "127.0.0.1:$p", 'request - authority'); + +($frame) = grep { $_->{type} eq "DATA" } @$frames; +is($frame->{data}, 'Hello', 'request - DATA'); +is($frame->{length}, 5, 'request - DATA length'); +is($frame->{flags}, 1, 'request - DATA flags'); + +$frames = $f->{http_end}(); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{headers}{'x-body'}, 'Hello', 'request body in memory'); + +# tcp_nopush usage on peer connections +# reopen window for request body after initial window was exhausted + +$frames = $f->{http_start}('/proxy'); +is(eval(join '+', map { $_->{length} } grep { $_->{type} eq "DATA" } @$frames), + 65535, 'preserve_output - first body bytes'); + +# expect body cleanup is disabled with preserve_output (ticket #1565). +# after request body first bytes were proxied on behalf of initial window size, +# send response header from upstream, this leads to body cleanup code path + +$frames = $f->{http_end}(); +is(eval(join '+', map { $_->{length} } grep { $_->{type} eq "DATA" } @$frames), + 465, 'preserve_output - last body bytes'); + +like(`grep -F '[crit]' ${\($t->testdir())}/error.log`, qr/^$/s, 'no crits'); + +############################################################################### + +sub proxy_http2 { + my ($server, $client, $f, $s, $c, $sid, $uri); + + $server = IO::Socket::INET->new( + Proto => 'tcp', + LocalHost => '127.0.0.1', + LocalPort => $p, + Listen => 5, + Reuse => 1 + ) + or die "Can't create listening socket: $!\n"; + + $f->{http_start} = sub { + ($uri, my %extra) = @_; + $s = Test::Nginx::HTTP2->new() if !defined $s; + my ($body) = $uri eq '/proxy' ? 'Hello' x 13200 : 'Hello'; + $s->new_stream({ body => $body, headers => [ + { name => ':method', value => 'POST', mode => 0 }, + { name => ':scheme', value => 'http', mode => 0 }, + { name => ':path', value => $uri }, + { name => ':authority', value => 'localhost' }, + { name => 'content-type', value => 'text/plain' }, + { name => 'te', value => 'trailers', mode => 2 }, + { name => 'content-length', value => length($body) }]}); + + if (!$extra{reuse}) { + eval { + local $SIG{ALRM} = sub { die "timeout\n" }; + alarm(5); + + $client = $server->accept() or return; + + alarm(0); + }; + alarm(0); + if ($@) { + log_in("died: $@"); + return undef; + } + + log2c("(new connection $client)"); + + $client->sysread(my $buf, 24) == 24 or return; # preface + + $c = Test::Nginx::HTTP2->new(1, socket => $client, + pure => 1, preface => "") or return; + } + + my $frames = $uri eq '/proxy' + ? $c->read(all => [{ length => 65535 }]) + : $c->read(all => [{ fin => 1 }]); + + if (!$extra{reuse}) { + $c->h2_settings(0); + $c->h2_settings(1); + } + + my ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; + $sid = $frame->{sid}; + return $frames; + }; + $f->{http_end} = sub { + $c->new_stream({ body_more => 1, headers => [ + { name => ':status', value => '200', mode => 0 }, + { name => 'content-type', value => 'text/plain' }, + ]}, $sid); + + # reopen window for request body after response HEADERS is sent + + if ($uri eq '/proxy') { + $c->h2_window(2**16, $sid); + $c->h2_window(2**16); + return $c->read(all => [{ sid => $sid, fin => 1 }]); + } + + $c->h2_body('Hello world', { body_more => 1 }); + $c->new_stream({ headers => [ + { name => 'x-status', value => '0', mode => 2 }, + { name => 'x-message', value => '', mode => 2 }, + ]}, $sid); + + return $s->read(all => [{ fin => 1 }]); + }; + return $f; +} + +sub log2c { Test::Nginx::log_core('||', @_); } + +############################################################################### From a0587b65b6d7f72c2413a949d2f958c5b8c219c1 Mon Sep 17 00:00:00 2001 From: Zhidao HONG Date: Mon, 27 Oct 2025 03:05:04 +0000 Subject: [PATCH 3/7] Tests: add proxy_http2_pass.t for HTTP/2 proxy with variables. --- proxy_http2_pass.t | 219 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 219 insertions(+) create mode 100644 proxy_http2_pass.t diff --git a/proxy_http2_pass.t b/proxy_http2_pass.t new file mode 100644 index 00000000..9c068e73 --- /dev/null +++ b/proxy_http2_pass.t @@ -0,0 +1,219 @@ +#!/usr/bin/perl + +# (C) Sergey Kandaurov +# (C) Nginx, Inc. + +# Tests for the proxy_pass directive with HTTP/2 and variables. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +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 http_ssl http_v2 proxy rewrite/) + ->has_daemon('openssl')->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + upstream u { + server 127.0.0.1:8081; + } + + resolver 127.0.0.1:%%PORT_8982_UDP%%; + + server { + listen 127.0.0.1:8080; + server_name localhost; + + location / { + proxy_pass http://$host:%%PORT_8081%%; + proxy_http_version 2; + } + + location /http { + proxy_pass http://$host:%%PORT_8081%%; + proxy_http_version 2; + } + + location /https { + proxy_pass https://$host:%%PORT_8082%%; + proxy_http_version 2; + } + + location /arg { + proxy_pass $arg_b; + proxy_http_version 2; + } + + location /stub { + proxy_pass http://127.0.0.1:%%PORT_8081%%/backend; + proxy_http_version 2; + } + + location /upath { + proxy_pass http://u/backend; + proxy_http_version 2; + } + } + + server { + listen 127.0.0.1:8081 http2; + listen 127.0.0.1:8082 http2 ssl; + server_name localhost; + + ssl_certificate_key localhost.key; + ssl_certificate localhost.crt; + + location / { + return 200 $http_host; + } + + location /backend { + return 200 "SEE-THIS"; + } + } +} + +EOF + +$t->write_file('openssl.conf', <testdir(); + +foreach my $name ('localhost') { + system('openssl req -x509 -new ' + . "-config $d/openssl.conf -subj /CN=$name/ " + . "-out $d/$name.crt -keyout $d/$name.key " + . ">>$d/openssl.out 2>&1") == 0 + or die "Can't create certificate for $name: $!\n"; +} + +$t->run_daemon(\&dns_daemon, port(8982), $t); + +# suppress deprecation warning + +open OLDERR, ">&", \*STDERR; close STDERR; +$t->run()->plan(7); +open STDERR, ">&", \*OLDERR; + +$t->waitforfile($t->testdir . '/' . port(8982)); + +############################################################################### + +like(http_get('/basic'), qr/200 OK/, 'no scheme'); +like(http_get('/http'), qr/200 OK/, 'http scheme'); + +SKIP: { +skip 'OpenSSL too old', 1 + if $t->has_module('OpenSSL') + and not $t->has_feature('openssl:1.0.2'); + +like(http_get('/https'), qr/200 OK/, 'https scheme'); + +} + +like(http_get('/arg?b=http://127.0.0.1:' . port(8081)), qr/200 OK/, 'addrs'); +like(http_get('/arg?b=http://u'), qr/200 OK/, 'no_port'); + +like(http_get('/stub'), qr/SEE-THIS/, 'proxy_pass with uri'); +like(http_get('/upath'), qr/SEE-THIS/, 'proxy_pass with uri to upstream'); + +############################################################################### + +sub reply_handler { + my ($recv_data) = @_; + + my (@name, @rdata); + + use constant NOERROR => 0; + use constant A => 1; + use constant IN => 1; + + # default values + + my ($hdr, $rcode, $ttl) = (0x8180, NOERROR, 3600); + + # decode name + + my ($len, $offset) = (undef, 12); + while (1) { + $len = unpack("\@$offset C", $recv_data); + last if $len == 0; + $offset++; + push @name, unpack("\@$offset A$len", $recv_data); + $offset += $len; + } + + $offset -= 1; + my ($id, $type, $class) = unpack("n x$offset n2", $recv_data); + + my $name = join('.', @name); + if ($name eq 'localhost' && $type == A) { + push @rdata, rd_addr($ttl, '127.0.0.1'); + } + + $len = @name; + pack("n6 (C/a*)$len x n2", $id, $hdr | $rcode, 1, scalar @rdata, + 0, 0, @name, $type, $class) . join('', @rdata); +} + +sub rd_addr { + my ($ttl, $addr) = @_; + + my $code = 'split(/\./, $addr)'; + + return pack 'n3N', 0xc00c, A, IN, $ttl if $addr eq ''; + + pack 'n3N nC4', 0xc00c, A, IN, $ttl, eval "scalar $code", eval($code); +} + +sub dns_daemon { + my ($port, $t) = @_; + + my ($data, $recv_data); + my $socket = IO::Socket::INET->new( + LocalAddr => '127.0.0.1', + LocalPort => $port, + Proto => 'udp', + ) + or die "Can't create listening socket: $!\n"; + + # signal we are ready + + open my $fh, '>', $t->testdir() . '/' . $port; + close $fh; + + while (1) { + $socket->recv($recv_data, 65536); + $data = reply_handler($recv_data); + $socket->send($data); + } +} + +############################################################################### From 06b56acec7de11480ca00cd52216574024969c6d Mon Sep 17 00:00:00 2001 From: Zhidao HONG Date: Mon, 27 Oct 2025 03:17:18 +0000 Subject: [PATCH 4/7] Tests: add proxy_http2_ssl.t for HTTP/2 proxy with SSL support. --- proxy_http2_ssl.t | 352 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 352 insertions(+) create mode 100644 proxy_http2_ssl.t diff --git a/proxy_http2_ssl.t b/proxy_http2_ssl.t new file mode 100644 index 00000000..d5318532 --- /dev/null +++ b/proxy_http2_ssl.t @@ -0,0 +1,352 @@ +#!/usr/bin/perl + +# (C) Sergey Kandaurov +# (C) Nginx, Inc. + +# Tests for HTTP/2 proxy backend with ssl. + +############################################################################### + +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 rewrite http_v2 proxy/) + ->has(qw/upstream_keepalive http_ssl/) + ->has_daemon('openssl'); + +plan(skip_all => 'no ALPN support in OpenSSL') + if $t->has_module('OpenSSL') and not $t->has_feature('openssl:1.0.2'); + +$t->write_file_expand('nginx.conf', <<'EOF')->plan(38); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + upstream u { + server 127.0.0.1:8081; + keepalive 1; + } + + server { + listen 127.0.0.1:8081 http2 ssl; + server_name localhost; + + ssl_certificate_key localhost.key; + ssl_certificate localhost.crt; + + ssl_verify_client optional; + ssl_client_certificate client.crt; + + http2_body_preread_size 128k; + + location / { + proxy_pass http://127.0.0.1:8082; + proxy_http_version 2; + proxy_request_buffering off; + proxy_set_header TE "trailers"; + proxy_pass_trailers on; + add_header X-Connection $connection; + } + } + + server { + listen 127.0.0.1:8080 http2; + server_name localhost; + + http2_body_preread_size 128k; + + location / { + proxy_pass https://127.0.0.1:8081; + proxy_http_version 2; + proxy_request_buffering off; + proxy_set_header TE "trailers"; + proxy_pass_trailers on; + proxy_ssl_name localhost; + proxy_ssl_verify on; + proxy_ssl_trusted_certificate localhost.crt; + + proxy_ssl_certificate client.crt; + proxy_ssl_certificate_key client.key; + proxy_ssl_password_file password; + + if ($arg_if) { + # nothing + } + + limit_except GET { + # nothing + } + } + + location /KeepAlive { + proxy_pass https://u; + proxy_http_version 2; + proxy_request_buffering off; + proxy_set_header TE "trailers"; + proxy_pass_trailers on; + } + } +} + +EOF + +$t->write_file('openssl.conf', <testdir(); + +foreach my $name ('localhost') { + system('openssl req -x509 -new ' + . "-config $d/openssl.conf -subj /CN=$name/ " + . "-out $d/$name.crt -keyout $d/$name.key " + . ">>$d/openssl.out 2>&1") == 0 + or die "Can't create certificate for $name: $!\n"; +} + +foreach my $name ('client') { + system("openssl genrsa -out $d/$name.key -passout pass:$name " + . "-aes128 2048 >>$d/openssl.out 2>&1") == 0 + or die "Can't create private key: $!\n"; + system('openssl req -x509 -new ' + . "-config $d/openssl.conf -subj /CN=$name/ " + . "-out $d/$name.crt " + . "-key $d/$name.key -passin pass:$name" + . ">>$d/openssl.out 2>&1") == 0 + or die "Can't create certificate for $name: $!\n"; +} + +sleep 1 if $^O eq 'MSWin32'; + +$t->write_file('password', 'client'); + +# suppress deprecation warning + +open OLDERR, ">&", \*STDERR; close STDERR; +$t->run(); +open STDERR, ">&", \*OLDERR; + +############################################################################### + +my $p = port(8082); +my $f = proxy(); + +my $frames = $f->{http_start}('/SayHello'); +my ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{flags}, 4, 'request - HEADERS flags'); +ok((my $sid = $frame->{sid}) % 2, 'request - HEADERS sid odd'); +is($frame->{headers}{':method'}, 'POST', 'request - method'); +is($frame->{headers}{':scheme'}, 'http', 'request - scheme'); +is($frame->{headers}{':path'}, '/SayHello', 'request - path'); +is($frame->{headers}{':authority'}, "127.0.0.1:$p", 'request - authority'); +is($frame->{headers}{'content-type'}, 'text/plain', + 'request - content type'); +is($frame->{headers}{te}, 'trailers', 'request - te'); + +$frames = $f->{data}('Hello'); +($frame) = grep { $_->{type} eq "SETTINGS" } @$frames; +is($frame->{flags}, 1, 'request - SETTINGS ack'); +is($frame->{sid}, 0, 'request - SETTINGS sid'); +is($frame->{length}, 0, 'request - SETTINGS length'); + +($frame) = grep { $_->{type} eq "DATA" } @$frames; +is($frame->{data}, 'Hello', 'request - DATA'); +is($frame->{length}, 5, 'request - DATA length'); +is($frame->{flags}, 1, 'request - DATA flags'); +is($frame->{sid}, $sid, 'request - DATA sid match'); + +$frames = $f->{http_end}(); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{flags}, 4, 'response - HEADERS flags'); +is($frame->{sid}, 1, 'response - HEADERS sid'); +is($frame->{headers}{':status'}, '200', 'response - status'); +is($frame->{headers}{'content-type'}, 'text/plain', + 'response - content type'); +ok($frame->{headers}{server}, 'response - server'); +ok($frame->{headers}{date}, 'response - date'); +ok(my $c = $frame->{headers}{'x-connection'}, 'response - connection'); + +($frame) = grep { $_->{type} eq "DATA" } @$frames; +is($frame->{data}, 'Hello world', 'response - DATA'); +is($frame->{length}, 11, 'response - DATA length'); +is($frame->{flags}, 0, 'response - DATA flags'); +is($frame->{sid}, 1, 'response - DATA sid'); + +(undef, $frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{flags}, 5, 'response - trailers flags'); +is($frame->{sid}, 1, 'response - trailers sid'); +is($frame->{headers}{'x-message'}, '', 'response - trailers message'); +is($frame->{headers}{'x-status'}, '0', 'response - trailers status'); + +# next request is on a new backend connection, no sid incremented + +$f->{http_start}('/SayHello'); +$f->{data}('Hello'); +$frames = $f->{http_end}(); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +cmp_ok($frame->{headers}{'x-connection'}, '>', $c, 'response 2 - connection'); + +# flow control + +$f->{http_start}('/FlowControl'); +$frames = $f->{data_len}(('Hello' x 13000) . ('x' x 550), 65535); +my $sum = eval join '+', map { $_->{type} eq "DATA" && $_->{length} } @$frames; +is($sum, 65535, 'flow control - iws length'); + +$f->{update}(10); +$f->{update_sid}(10); + +$frames = $f->{data_len}(undef, 10); +($frame) = grep { $_->{type} eq "DATA" } @$frames; +is($frame->{length}, 10, 'flow control - update length'); +is($frame->{flags}, 0, 'flow control - update flags'); + +$f->{update_sid}(10); +$f->{update}(10); + +$frames = $f->{data_len}(undef, 5); +($frame) = grep { $_->{type} eq "DATA" } @$frames; +is($frame->{length}, 5, 'flow control - rest length'); +is($frame->{flags}, 1, 'flow control - rest flags'); + +$f->{http_end}(); + +# upstream keepalive + +$f->{http_start}('/KeepAlive'); +$f->{data}('Hello'); +$frames = $f->{http_end}(); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +ok($c = $frame->{headers}{'x-connection'}, 'keepalive - connection'); + +$f->{http_start}('/KeepAlive'); +$f->{data}('Hello'); +$frames = $f->{http_end}(); +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{headers}{'x-connection'}, $c, 'keepalive - connection reuse'); + +############################################################################### + +sub proxy { + my ($server, $client, $f, $s, $c, $sid, $uri); + + $server = IO::Socket::INET->new( + Proto => 'tcp', + LocalHost => '127.0.0.1', + LocalPort => $p, + Listen => 5, + Reuse => 1 + ) + or die "Can't create listening socket: $!\n"; + + $f->{http_start} = sub { + ($uri, my %extra) = @_; + my $body_more = 1 if $uri !~ /LongHeader/; + $s = Test::Nginx::HTTP2->new() if !defined $s; + $s->new_stream({ body_more => $body_more, headers => [ + { name => ':method', value => 'POST', mode => 0 }, + { name => ':scheme', value => 'http', mode => 0 }, + { name => ':path', value => $uri, }, + { name => ':authority', value => 'localhost' }, + { name => 'content-type', value => 'text/plain' }, + { name => 'te', value => 'trailers', mode => 2 }]}); + + if (!$extra{reuse}) { + eval { + local $SIG{ALRM} = sub { die "timeout\n" }; + alarm(5); + + $client = $server->accept() or return; + + alarm(0); + }; + alarm(0); + if ($@) { + log_in("died: $@"); + return undef; + } + + log2c("(new connection $client)"); + + $client->sysread(my $buf, 24) == 24 or return; # preface + + $c = Test::Nginx::HTTP2->new(1, socket => $client, + pure => 1, preface => "") or return; + } + + my $frames = $c->read(all => [{ fin => 4 }]); + + if (!$extra{reuse}) { + $c->h2_settings(0); + $c->h2_settings(1); + } + + my ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; + $sid = $frame->{sid}; + return $frames; + }; + $f->{data_len} = sub { + my ($body, $len) = @_; + $s->h2_body($body) if defined $body; + return $c->read(all => [{ sid => $sid, length => $len }]); + }; + $f->{update} = sub { + $c->h2_window(shift); + }; + $f->{update_sid} = sub { + $c->h2_window(shift, $sid); + }; + $f->{data} = sub { + my ($body, %extra) = @_; + $s->h2_body($body, { %extra }); + return $c->read(all => [{ sid => $sid, + length => length($body) }]); + }; + $f->{http_end} = sub { + $c->new_stream({ body_more => 1, headers => [ + { name => ':status', value => '200', mode => 0 }, + { name => 'content-type', value => 'text/plain', + mode => 1, huff => 1 }, + ]}, $sid); + $c->h2_body('Hello world', { body_more => 1 }); + $c->new_stream({ headers => [ + { name => 'x-status', value => '0', + mode => 2, huff => 1 }, + { name => 'x-message', value => '', + mode => 2, huff => 1 }, + ]}, $sid); + + return $s->read(all => [{ fin => 1 }]); + }; + return $f; +} + +sub log2i { Test::Nginx::log_core('|| <<', @_); } +sub log2o { Test::Nginx::log_core('|| >>', @_); } +sub log2c { Test::Nginx::log_core('||', @_); } + +############################################################################### From d35ad4ed2a1c60bb246a0c381c6eb3c9deab07d7 Mon Sep 17 00:00:00 2001 From: Zhidao HONG Date: Mon, 27 Oct 2025 03:29:55 +0000 Subject: [PATCH 5/7] Tests: add proxy_http2_next_upstream.t for HTTP/2 proxy next_upstream directive. --- proxy_http2_next_upstream.t | 158 ++++++++++++++++++++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 proxy_http2_next_upstream.t diff --git a/proxy_http2_next_upstream.t b/proxy_http2_next_upstream.t new file mode 100644 index 00000000..72ef0890 --- /dev/null +++ b/proxy_http2_next_upstream.t @@ -0,0 +1,158 @@ +#!/usr/bin/perl + +# (C) Maxim Dounin +# (C) Sergey Kandaurov +# (C) Nginx, Inc. + +# Tests for HTTP/2 proxy module, proxy_next_upstream directive. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +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 http_v2 proxy rewrite/)->plan(9); + +$t->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + upstream u { + server 127.0.0.1:8081 max_fails=2; + server 127.0.0.1:8082; + } + + upstream u2 { + server 127.0.0.1:8081; + server 127.0.0.1:8082; + } + + server { + listen 127.0.0.1:8080; + server_name localhost; + + location / { + proxy_pass http://u; + proxy_http_version 2; + proxy_request_buffering off; + proxy_set_header TE "trailers"; + proxy_pass_trailers on; + proxy_next_upstream http_500 http_404 invalid_header; + } + + location /all/ { + proxy_pass http://u2; + proxy_http_version 2; + proxy_request_buffering off; + proxy_set_header TE "trailers"; + proxy_pass_trailers on; + proxy_next_upstream http_500 http_404; + error_page 404 /all/404; + proxy_intercept_errors on; + } + + location /all/404 { + return 200 "$upstream_addr\n"; + } + } + + server { + listen 127.0.0.1:8081 http2; + server_name localhost; + + location / { + return 404; + } + location /ok { + return 200 "AND-THIS\n"; + } + location /500 { + return 500; + } + location /444 { + return 444; + } + + location /all/ { + return 404; + } + } + + server { + listen 127.0.0.1:8082 http2; + server_name localhost; + + location / { + return 200 "TEST-OK-IF-YOU-SEE-THIS\n"; + } + + location /all/ { + return 404; + } + } +} + +EOF + +# suppress deprecation warning + +open OLDERR, ">&", \*STDERR; close STDERR; +$t->run(); +open STDERR, ">&", \*OLDERR; + +############################################################################### + +my ($p1, $p2) = (port(8081), port(8082)); + +# check if both request fallback to a backend +# which returns valid response + +like(http_get('/'), qr/SEE-THIS/, 'proxy request'); +like(http_get('/'), qr/SEE-THIS/, 'second request'); + +# make sure backend isn't switched off after +# proxy_next_upstream http_404 + +like(http_get('/ok') . http_get('/ok'), qr/AND-THIS/, 'not down'); + +# next upstream on invalid_header + +like(http_get('/444'), qr/SEE-THIS/, 'request 444'); +like(http_get('/444'), qr/SEE-THIS/, 'request 444 second'); + +# next upstream on http_500 + +like(http_get('/500'), qr/SEE-THIS/, 'request 500'); +like(http_get('/500'), qr/SEE-THIS/, 'request 500 second'); + +# make sure backend switched off with http_500 + +unlike(http_get('/ok') . http_get('/ok'), qr/AND-THIS/, 'down after 500'); + +# make sure all backends are tried once + +like(http_get('/all/rr'), + qr/^127.0.0.1:($p1, 127.0.0.1:$p2|$p2, 127.0.0.1:$p1)$/mi, + 'all tried once'); + +############################################################################### From df897e73d92b74779b38afb26555ca471a1fee5c Mon Sep 17 00:00:00 2001 From: Zhidao HONG Date: Wed, 19 Nov 2025 05:19:58 +0000 Subject: [PATCH 6/7] Tests: add proxy_http2_cache.t for HTTP/2 proxy with cache support. This is a basic test for cache functionality. More comprehensive tests will be added later. --- proxy_http2_cache.t | 32 ++++++++++++-------------------- 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/proxy_http2_cache.t b/proxy_http2_cache.t index bc85ee56..f9bb231f 100644 --- a/proxy_http2_cache.t +++ b/proxy_http2_cache.t @@ -24,7 +24,7 @@ select STDERR; $| = 1; select STDOUT; $| = 1; my $t = Test::Nginx->new()->has(qw/http rewrite http_v2 proxy cache/) - ->has(qw/upstream_keepalive/)->plan(2); + ->has(qw/upstream_keepalive/)->plan(3); $t->write_file_expand('nginx.conf', <<'EOF'); @@ -47,7 +47,7 @@ http { location / { proxy_pass http://127.0.0.1:8081; - proxy_http_version 2.0; + proxy_http_version 2; proxy_request_buffering off; proxy_set_header TE "trailers"; proxy_pass_trailers on; @@ -84,17 +84,11 @@ $frames = $f->{http_end}(); ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; like($frame->{headers}{'x-cache-status'}, qr/MISS/, 'cache test - MISS on first request'); -# Test cached response - second request should be HIT +# Second request - should be HIT from cache -undef $f; -$f = proxy_http2(); - -$frames = $f->{http_start}('/'); -($frame) = grep { $_->{type} eq "HEADERS" } @$frames; -$f->{data}('Hello'); -$frames = $f->{http_end}(); +$frames = $f->{request}('/'); ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; -like($frame->{headers}{'x-cache-status'}, qr/HIT/, 'cache test - HIT on cached request'); +like($frame->{headers}{'x-cache-status'}, qr/HIT/, 'cache test - HIT on second request'); ############################################################################### @@ -157,20 +151,12 @@ sub proxy_http2 { $f->{http_end} = sub { my (%extra) = @_; - # Determine cache status based on request count - my $cache_status = 'MISS'; - if ($n > 1) { - $cache_status = 'HIT'; - } - my $h = [ { name => ':status', value => '200', mode => $extra{mode} || 0 }, { name => 'content-type', value => 'text/plain', mode => $extra{mode} || 1, huff => 1 }, { name => 'x-connection', value => $n, - mode => 2, huff => 1 }, - { name => 'x-cache-status', value => $cache_status, mode => 2, huff => 1 }]; push @$h, { name => 'content-length', value => $extra{cl} } if $extra{cl}; @@ -187,6 +173,12 @@ sub proxy_http2 { return $s->read(all => [{ fin => 1 }]); }; + $f->{request} = sub { + my ($uri) = @_; + $s = Test::Nginx::HTTP2->new() if !defined $s; + my $sid = $s->new_stream({ path => $uri }); + return $s->read(all => [{ fin => 1 }]); + }; return $f; } @@ -194,4 +186,4 @@ sub log2i { Test::Nginx::log_core('|| <<', @_); } sub log2o { Test::Nginx::log_core('|| >>', @_); } sub log2c { Test::Nginx::log_core('||', @_); } -############################################################################### \ No newline at end of file +############################################################################### From 5a6a68f500823696460de5ce6abbe543928730ab Mon Sep 17 00:00:00 2001 From: Zhidao HONG Date: Tue, 2 Dec 2025 02:18:29 +0000 Subject: [PATCH 7/7] Tests: add improved HTTP/2 proxy tests based on standard proxy tests. --- proxy_http2_cache_2.t | 143 ++++++++++++++++++++++++++ proxy_http2_cache_convert_head_2.t | 103 +++++++++++++++++++ proxy_http2_non_idempotent.t | 158 +++++++++++++++++++++++++++++ proxy_http2_store_2.t | 116 +++++++++++++++++++++ 4 files changed, 520 insertions(+) create mode 100644 proxy_http2_cache_2.t create mode 100644 proxy_http2_cache_convert_head_2.t create mode 100644 proxy_http2_non_idempotent.t create mode 100644 proxy_http2_store_2.t diff --git a/proxy_http2_cache_2.t b/proxy_http2_cache_2.t new file mode 100644 index 00000000..7a2f8e1b --- /dev/null +++ b/proxy_http2_cache_2.t @@ -0,0 +1,143 @@ +#!/usr/bin/perl + +# (C) Zhidao HONG +# (C) Nginx, Inc. + +# Tests for HTTP/2 proxy backend with cache support. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +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 cache http_v2/) + ->plan(15); + +$t->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + proxy_cache_path %%TESTDIR%%/cache levels=1:2 + keys_zone=NAME:1m; + + server { + listen 127.0.0.1:8080; + server_name localhost; + + location / { + proxy_pass http://127.0.0.1:8081; + proxy_http_version 2; + + proxy_cache NAME; + + proxy_cache_valid 200 302 2s; + proxy_cache_valid 301 1d; + proxy_cache_valid any 1m; + + proxy_cache_min_uses 1; + + proxy_cache_use_stale error timeout invalid_header http_500 + http_404; + + proxy_no_cache $arg_e; + + add_header X-Cache-Status $upstream_cache_status; + } + } + + server { + listen 127.0.0.1:8081; + server_name localhost; + + http2 on; + + location / { + root %%TESTDIR%%; + } + } +} + +EOF + +$t->write_file('t.html', 'SEE-THIS'); +$t->write_file('t2.html', 'SEE-THIS'); +$t->write_file('empty.html', ''); +$t->write_file('big.html', 'x' x 1024); + +$t->run(); + +############################################################################### + +like(http_get('/t.html'), qr/SEE-THIS/, 'proxy request'); + +$t->write_file('t.html', 'NOOP'); +like(http_get('/t.html'), qr/SEE-THIS/, 'proxy request cached'); + +unlike(http_head('/t2.html'), qr/SEE-THIS/, 'head request'); +like(http_get('/t2.html'), qr/SEE-THIS/, 'get after head'); +unlike(http_head('/t2.html'), qr/SEE-THIS/, 'head after get'); + +like(http_head('/empty.html?head'), qr/MISS/, 'empty head first'); +like(http_head('/empty.html?head'), qr/HIT/, 'empty head second'); + +like(http_get_range('/t.html', 'Range: bytes=4-'), qr/^THIS/m, 'cached range'); +like(http_get_range('/t.html', 'Range: bytes=0-2,4-'), qr/^SEE.*^THIS/ms, + 'cached multipart range'); + +like(http_get('/empty.html'), qr/MISS/, 'empty get first'); +like(http_get('/empty.html'), qr/HIT/, 'empty get second'); + +select(undef, undef, undef, 3.1); +unlink $t->testdir() . '/t.html'; +like(http_get('/t.html'), qr/STALE/, 'non-empty get stale'); + +unlink $t->testdir() . '/empty.html'; +like(http_get('/empty.html'), qr/STALE/, 'empty get stale'); + +# no client connection close with response on non-cacheable HEAD requests + +my $s = http(< 1); +HEAD /big.html?e=1 HTTP/1.1 +Host: localhost + +EOF + +my $r = http_get('/t.html', socket => $s); + +like($r, qr/Connection: keep-alive/, 'non-cacheable head - keepalive'); +like($r, qr/SEE-THIS/, 'non-cacheable head - second'); + +############################################################################### + +sub http_get_range { + my ($url, $extra) = @_; + return http(<new()->has(qw/http proxy cache http_v2/)->plan(8) + ->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + proxy_cache_path %%TESTDIR%%/cache levels=1:2 + keys_zone=NAME:1m; + + server { + listen 127.0.0.1:8080; + server_name localhost; + + proxy_cache NAME; + + proxy_cache_key $request_uri; + + proxy_cache_valid 200 302 2s; + + add_header X-Cache-Status $upstream_cache_status; + + location / { + proxy_pass http://127.0.0.1:8081/t.html; + proxy_http_version 2; + proxy_cache_convert_head off; + + location /inner { + proxy_pass http://127.0.0.1:8081/t.html; + proxy_http_version 2; + proxy_cache_convert_head on; + } + } + + location /on { + proxy_pass http://127.0.0.1:8081/t.html; + proxy_http_version 2; + proxy_cache_convert_head on; + } + } + + server { + listen 127.0.0.1:8081; + server_name localhost; + + http2 on; + + location / { + root %%TESTDIR%%; + add_header X-Method $request_method; + } + } +} + +EOF + +$t->write_file('t.html', 'SEE-THIS'); +$t->run(); + +############################################################################### + +like(http_get('/'), qr/x-method: GET/i, 'get'); +like(http_head('/?2'), qr/x-method: HEAD/i, 'head'); +like(http_head('/?2'), qr/HIT/, 'head cached'); +unlike(http_get('/?2'), qr/SEE-THIS/, 'get after head'); + +like(http_get('/on'), qr/x-method: GET/i, 'on - get'); +like(http_head('/on?2'), qr/x-method: GET/i, 'on - head'); + +like(http_get('/inner'), qr/x-method: GET/i, 'inner - get'); +like(http_head('/inner?2'), qr/x-method: GET/i, 'inner - head'); + +############################################################################### diff --git a/proxy_http2_non_idempotent.t b/proxy_http2_non_idempotent.t new file mode 100644 index 00000000..8fa28e34 --- /dev/null +++ b/proxy_http2_non_idempotent.t @@ -0,0 +1,158 @@ +#!/usr/bin/perl + +# (C) Zhidao HONG +# (C) Nginx, Inc. + +# Tests for HTTP/2 proxy backend with proxy_next_upstream non_idempotent. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +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 upstream_keepalive http_v2/) + ->plan(8); + +$t->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + upstream u { + server 127.0.0.1:8081 max_fails=0; + server 127.0.0.1:8082 max_fails=0; + } + + upstream uk { + server 127.0.0.1:8081 max_fails=0; + server 127.0.0.1:8082 max_fails=0; + keepalive 10; + } + + server { + listen 127.0.0.1:8080; + server_name localhost; + + add_header X-IP $upstream_addr always; + + location / { + proxy_pass http://u; + proxy_http_version 2; + proxy_next_upstream error timeout http_500; + } + + location /non { + proxy_pass http://u; + proxy_http_version 2; + proxy_next_upstream error timeout http_500 non_idempotent; + } + + location /keepalive { + proxy_pass http://uk; + proxy_http_version 2; + proxy_next_upstream error timeout; + } + } + + server { + listen 127.0.0.1:8081; + server_name localhost; + + http2 on; + + location / { + return 500; + } + + location /500 { + return 500 SEE-THIS; + } + + location /keepalive/establish { + return 204; + } + } + + server { + listen 127.0.0.1:8082; + server_name localhost; + + http2 on; + + location / { + return 500; + } + + location /500 { + return 500 SEE-THIS; + } + + location /keepalive/drop { + return 500; + } + } +} + +EOF + +$t->run(); + +############################################################################### + +# non-idempotent requests should not be retried by default +# if a request has been sent to a backend + +like(http_get('/'), qr/x-ip: (\S+), (\S+)\x0d?$/mi, 'get'); +like(http_post('/'), qr/x-ip: (\S+)\x0d?$/mi, 'post'); + +# non-idempotent requests should not be retried by default, +# in particular, not emit builtin error page due to next upstream + +like(http_get('/500'), qr/x-ip: (\S+), (\S+).*SEE-THIS/si, 'get 500'); +like(http_post('/500'), qr/x-ip: (\S++)(?! ).*SEE-THIS/si, 'post 500'); + +# with "proxy_next_upstream non_idempotent" there is no +# difference between idempotent and non-idempotent requests, +# non-idempotent requests are retried as usual + +like(http_get('/non'), qr/x-ip: (\S+), (\S+)\x0d?$/mi, 'get non_idempotent'); +like(http_post('/non'), qr/x-ip: (\S+), (\S+)\x0d?$/mi, 'post non_idempotent'); + +# cached connections follow the same rules + +like(http_get('/keepalive/establish'), qr/204 No Content/mi, 'keepalive'); +like(http_post('/keepalive/drop'), qr/x-ip: (\S+)\x0d?$/mi, 'keepalive post'); + +############################################################################### + +sub http_post { + my ($uri, %extra) = @_; + my $cl = $extra{cl} || 0; + + http(<<"EOF"); +POST $uri HTTP/1.0 +Content-Length: $cl + +EOF +} + +############################################################################### diff --git a/proxy_http2_store_2.t b/proxy_http2_store_2.t new file mode 100644 index 00000000..b872ef39 --- /dev/null +++ b/proxy_http2_store_2.t @@ -0,0 +1,116 @@ +#!/usr/bin/perl + +# (C) Zhidao HONG +# (C) Nginx, Inc. + +# Tests for HTTP/2 proxy backend with proxy_store functionality. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +my $t = Test::Nginx->new(); + +$t->write_file_expand('nginx.conf', <<'EOF')->has(qw/http proxy ssi http_v2/)->plan(9); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + server { + listen 127.0.0.1:8080; + server_name localhost; + + location /store- { + proxy_pass http://127.0.0.1:8081/; + proxy_http_version 2; + proxy_store on; + } + location /store-string- { + proxy_pass http://127.0.0.1:8081/; + proxy_http_version 2; + proxy_store %%TESTDIR%%$uri; + } + location /ssi.html { + ssi on; + } + location /index-big.html { + limit_rate 200k; + } + } + + server { + listen 127.0.0.1:8081; + server_name localhost; + + http2 on; + + location / { + root %%TESTDIR%%; + } + } +} + +EOF + +$t->write_file('index.html', 'SEE-THIS'); +$t->write_file('index-nostore.html', 'SEE-THIS'); +$t->write_file('index-big.html', 'x' x (100 << 10)); +$t->write_file('ssi.html', + '' . + '' +); +$t->run(); + +############################################################################### + +like(http_get('/store-index.html'), qr/SEE-THIS/, 'proxy request'); +ok(-e $t->testdir() . '/store-index.html', 'result stored'); + +like(http_get('/store-string-index.html'), qr/SEE-THIS/, + 'proxy string path request'); +ok(-e $t->testdir() . '/store-string-index.html', 'string path result stored'); + +like(http_head('/store-index-nostore.html'), qr/200 OK/, 'head request'); +ok(!-e $t->testdir() . '/store-index-nostore.html', 'result not stored'); + +ok(scalar @{[ glob $t->testdir() . '/proxy_temp/*' ]} == 0, 'no temp files'); + +http_get('/store-index-big.html', aborted => 1, sleep => 0.1); + +select(undef, undef, undef, 0.5); +select(undef, undef, undef, 2.5) + if scalar @{[ glob $t->testdir() . '/proxy_temp/*' ]}; + +ok(scalar @{[ glob $t->testdir() . '/proxy_temp/*' ]} == 0, + 'no temp files after aborted request'); + +http_get('/ssi.html', aborted => 1, sleep => 0.1); + +select(undef, undef, undef, 0.5); +select(undef, undef, undef, 2.5) + if scalar @{[ glob $t->testdir() . '/proxy_temp/*' ]}; + +ok(scalar @{[ glob $t->testdir() . '/proxy_temp/*' ]} == 0, + 'no temp files after aborted ssi'); + +###############################################################################