You are viewing a plain text version of this content. The canonical link for it is here.
Posted to cvs@httpd.apache.org by ic...@apache.org on 2022/10/11 14:54:08 UTC

svn commit: r1904522 [2/2] - in /httpd/httpd/trunk: modules/http2/ test/modules/http2/ test/modules/http2/htdocs/cgi/ test/modules/http2/mod_h2test/ test/pyhttpd/

Modified: httpd/httpd/trunk/test/modules/http2/mod_h2test/mod_h2test.c
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/test/modules/http2/mod_h2test/mod_h2test.c?rev=1904522&r1=1904521&r2=1904522&view=diff
==============================================================================
--- httpd/httpd/trunk/test/modules/http2/mod_h2test/mod_h2test.c (original)
+++ httpd/httpd/trunk/test/modules/http2/mod_h2test/mod_h2test.c Tue Oct 11 14:54:08 2022
@@ -280,7 +280,7 @@ static int h2test_delay_handler(request_
 
 cleanup:
     ap_log_rerror(APLOG_MARK, APLOG_TRACE1, rv, r,
-                  "delay_handler: request cleanup, r->status=%d, aborted=%d",
+                  "delay_handler: request cleanup, r->status=%d, aborte=%d",
                   r->status, c->aborted);
     if (rv == APR_SUCCESS
         || r->status != HTTP_OK
@@ -297,7 +297,6 @@ static int h2test_trailer_handler(reques
     apr_bucket *b;
     apr_status_t rv;
     char buffer[8192];
-    int i, chunks = 3;
     long l;
     int body_len = 0;
 
@@ -345,7 +344,7 @@ static int h2test_trailer_handler(reques
 
 cleanup:
     ap_log_rerror(APLOG_MARK, APLOG_TRACE1, rv, r,
-                  "trailer_handler: request cleanup, r->status=%d, aborted=%d",
+                  "trailer_handler: request cleanup, r->status=%d, aborte=%d",
                   r->status, c->aborted);
     if (rv == APR_SUCCESS
         || r->status != HTTP_OK
@@ -355,6 +354,154 @@ cleanup:
     return AP_FILTER_ERROR;
 }
 
+static int status_from_str(const char *s, apr_status_t *pstatus)
+{
+    if (!strcmp("timeout", s)) {
+        *pstatus = APR_TIMEUP;
+        return 1;
+    }
+    else if (!strcmp("reset", s)) {
+        *pstatus = APR_ECONNRESET;
+        return 1;
+    }
+    return 0;
+}
+
+static int h2test_error_handler(request_rec *r)
+{
+    conn_rec *c = r->connection;
+    apr_bucket_brigade *bb;
+    apr_bucket *b;
+    apr_status_t rv;
+    char buffer[8192];
+    int i, chunks = 3, error_bucket = 1;
+    long l;
+    apr_time_t delay = 0, body_delay = 0;
+    apr_array_header_t *args = NULL;
+    int http_status = 200;
+    apr_status_t error = APR_SUCCESS, body_error = APR_SUCCESS;
+
+    if (strcmp(r->handler, "h2test-error")) {
+        return DECLINED;
+    }
+    if (r->method_number != M_GET && r->method_number != M_POST) {
+        return DECLINED;
+    }
+
+    if (r->args) {
+        args = apr_cstr_split(r->args, "&", 1, r->pool);
+        for (i = 0; i < args->nelts; ++i) {
+            char *s, *val, *arg = APR_ARRAY_IDX(args, i, char*);
+            s = strchr(arg, '=');
+            if (s) {
+                *s = '\0';
+                val = s + 1;
+                if (!strcmp("status", arg)) {
+                    http_status = (int)apr_atoi64(val);
+                    if (val > 0) {
+                        continue;
+                    }
+                }
+                else if (!strcmp("error", arg)) {
+                    if (status_from_str(val, &error)) {
+                        continue;
+                    }
+                }
+                else if (!strcmp("error_bucket", arg)) {
+                    error_bucket = (int)apr_atoi64(val);
+                    if (val >= 0) {
+                        continue;
+                    }
+                }
+                else if (!strcmp("body_error", arg)) {
+                    if (status_from_str(val, &body_error)) {
+                        continue;
+                    }
+                }
+                else if (!strcmp("delay", arg)) {
+                    rv = duration_parse(&delay, r->args, "s");
+                    if (APR_SUCCESS == rv) {
+                        continue;
+                    }
+                }
+                else if (!strcmp("body_delay", arg)) {
+                    rv = duration_parse(&body_delay, r->args, "s");
+                    if (APR_SUCCESS == rv) {
+                        continue;
+                    }
+                }
+            }
+            ap_die(HTTP_BAD_REQUEST, r);
+            return OK;
+        }
+    }
+
+    ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, "error_handler: processing request, %s",
+                  r->args? r->args : "(no args)");
+    r->status = http_status;
+    r->clength = -1;
+    r->chunked = 1;
+    apr_table_unset(r->headers_out, "Content-Length");
+    /* Discourage content-encodings */
+    apr_table_unset(r->headers_out, "Content-Encoding");
+    apr_table_setn(r->subprocess_env, "no-brotli", "1");
+    apr_table_setn(r->subprocess_env, "no-gzip", "1");
+
+    ap_set_content_type(r, "application/octet-stream");
+    bb = apr_brigade_create(r->pool, c->bucket_alloc);
+
+    if (delay) {
+        apr_sleep(delay);
+    }
+    if (error != APR_SUCCESS) {
+        return ap_map_http_request_error(error, HTTP_BAD_REQUEST);
+    }
+    /* flush response */
+    b = apr_bucket_flush_create(c->bucket_alloc);
+    APR_BRIGADE_INSERT_TAIL(bb, b);
+    rv = ap_pass_brigade(r->output_filters, bb);
+    if (APR_SUCCESS != rv) goto cleanup;
+
+    memset(buffer, 'X', sizeof(buffer));
+    l = sizeof(buffer);
+    for (i = 0; i < chunks; ++i) {
+        if (body_delay) {
+            apr_sleep(body_delay);
+        }
+        rv = apr_brigade_write(bb, NULL, NULL, buffer, l);
+        if (APR_SUCCESS != rv) goto cleanup;
+        rv = ap_pass_brigade(r->output_filters, bb);
+        if (APR_SUCCESS != rv) goto cleanup;
+        ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r,
+                      "error_handler: passed %ld bytes as response body", l);
+        if (body_error != APR_SUCCESS) {
+            rv = body_error;
+            goto cleanup;
+        }
+    }
+    /* we are done */
+    b = apr_bucket_eos_create(c->bucket_alloc);
+    APR_BRIGADE_INSERT_TAIL(bb, b);
+    rv = ap_pass_brigade(r->output_filters, bb);
+    apr_brigade_cleanup(bb);
+    ap_log_rerror(APLOG_MARK, APLOG_TRACE1, rv, r, "error_handler: response passed");
+
+cleanup:
+    ap_log_rerror(APLOG_MARK, APLOG_TRACE1, rv, r,
+                  "error_handler: request cleanup, r->status=%d, aborted=%d",
+                  r->status, c->aborted);
+    if (rv == APR_SUCCESS) {
+        return OK;
+    }
+    if (error_bucket) {
+        http_status = ap_map_http_request_error(rv, HTTP_BAD_REQUEST);
+        b = ap_bucket_error_create(http_status, NULL, r->pool, c->bucket_alloc);
+        APR_BRIGADE_INSERT_TAIL(bb, b);
+        ap_pass_brigade(r->output_filters, bb);
+    }
+    return AP_FILTER_ERROR;
+}
+
 /* Install this module into the apache2 infrastructure.
  */
 static void h2test_hooks(apr_pool_t *pool)
@@ -375,5 +522,6 @@ static void h2test_hooks(apr_pool_t *poo
     ap_hook_handler(h2test_echo_handler, NULL, NULL, APR_HOOK_MIDDLE);
     ap_hook_handler(h2test_delay_handler, NULL, NULL, APR_HOOK_MIDDLE);
     ap_hook_handler(h2test_trailer_handler, NULL, NULL, APR_HOOK_MIDDLE);
+    ap_hook_handler(h2test_error_handler, NULL, NULL, APR_HOOK_MIDDLE);
 }
 

Modified: httpd/httpd/trunk/test/modules/http2/test_003_get.py
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/test/modules/http2/test_003_get.py?rev=1904522&r1=1904521&r2=1904522&view=diff
==============================================================================
--- httpd/httpd/trunk/test/modules/http2/test_003_get.py (original)
+++ httpd/httpd/trunk/test/modules/http2/test_003_get.py Tue Oct 11 14:54:08 2022
@@ -237,3 +237,31 @@ content-type: text/html
         assert r.response['status'] == 200
         assert 'date' in r.response['header']
         assert 'server' in r.response['header']
+
+    # lets do some error tests
+    def test_h2_003_70(self, env):
+        url = env.mkurl("https", "cgi", "/h2test/error?status=500")
+        r = env.curl_get(url)
+        assert r.exit_code == 0, r
+        assert r.response['status'] == 500
+        url = env.mkurl("https", "cgi", "/h2test/error?error=timeout")
+        r = env.curl_get(url)
+        assert r.exit_code == 0, r
+        assert r.response['status'] == 408
+
+    # produce an error during response body
+    def test_h2_003_71(self, env, repeat):
+        pytest.skip("needs fix in core protocol handling")
+        url = env.mkurl("https", "cgi", "/h2test/error?body_error=timeout")
+        r = env.curl_get(url)
+        assert r.exit_code != 0, f"{r}"
+        url = env.mkurl("https", "cgi", "/h2test/error?body_error=reset")
+        r = env.curl_get(url)
+        assert r.exit_code != 0, f"{r}"
+
+    # produce an error, fail to generate an error bucket
+    def test_h2_003_72(self, env, repeat):
+        pytest.skip("needs fix in core protocol handling")
+        url = env.mkurl("https", "cgi", "/h2test/error?body_error=timeout&error_bucket=0")
+        r = env.curl_get(url)
+        assert r.exit_code != 0, f"{r}"

Modified: httpd/httpd/trunk/test/modules/http2/test_105_timeout.py
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/test/modules/http2/test_105_timeout.py?rev=1904522&r1=1904521&r2=1904522&view=diff
==============================================================================
--- httpd/httpd/trunk/test/modules/http2/test_105_timeout.py (original)
+++ httpd/httpd/trunk/test/modules/http2/test_105_timeout.py Tue Oct 11 14:54:08 2022
@@ -146,4 +146,4 @@ class TestTimeout:
                     break
             piper.close()
             assert piper.response
-            assert piper.response['status'] == 408
+            assert piper.response['status'] == 408, f"{piper.response}"

Modified: httpd/httpd/trunk/test/modules/http2/test_202_trailer.py
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/test/modules/http2/test_202_trailer.py?rev=1904522&r1=1904521&r2=1904522&view=diff
==============================================================================
--- httpd/httpd/trunk/test/modules/http2/test_202_trailer.py (original)
+++ httpd/httpd/trunk/test/modules/http2/test_202_trailer.py Tue Oct 11 14:54:08 2022
@@ -86,7 +86,7 @@ class TestTrailers:
         url = env.mkurl("https", "cgi", "/h2test/trailer?0")
         r = env.nghttp().get(url)
         assert r.response["status"] == 200
-        assert len(r.response["body"]) == 0
+        assert len(r.response["body"]) == 0, f'{r.response["body"]}'
         assert 'trailer' in r.response
         assert 'trailer-content-length' in r.response['trailer']
         assert r.response['trailer']['trailer-content-length'] == '0'

Added: httpd/httpd/trunk/test/modules/http2/test_203_rfc9113.py
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/test/modules/http2/test_203_rfc9113.py?rev=1904522&view=auto
==============================================================================
--- httpd/httpd/trunk/test/modules/http2/test_203_rfc9113.py (added)
+++ httpd/httpd/trunk/test/modules/http2/test_203_rfc9113.py Tue Oct 11 14:54:08 2022
@@ -0,0 +1,42 @@
+import pytest
+
+from pyhttpd.env import HttpdTestEnv
+from .env import H2Conf
+
+
+class TestRfc9113:
+
+    @pytest.fixture(autouse=True, scope='class')
+    def _class_scope(self, env):
+        H2Conf(env).add_vhost_test1().install()
+        assert env.apache_restart() == 0
+
+    # by default, we ignore leading/trailing ws
+    # tests with leading ws are not present as curl seems to silently eat those
+    def test_h2_203_01_ws_ignore(self, env):
+        url = env.mkurl("https", "test1", "/")
+        r = env.curl_get(url, options=['-H', 'trailing-space: must not  '])
+        assert r.exit_code == 0, f'curl output: {r.stderr}'
+        assert r.response["status"] == 200, f'curl output: {r.stdout}'
+        r = env.curl_get(url, options=['-H', 'trailing-space: must not\t'])
+        assert r.exit_code == 0, f'curl output: {r.stderr}'
+        assert r.response["status"] == 200, f'curl output: {r.stdout}'
+
+    # When enabled, leading/trailing make the stream RST
+    # tests with leading ws are not present as curl seems to silently eat those
+    def test_h2_203_02_ws_reject(self, env):
+        if not env.h2load_is_at_least('1.50.0'):
+            pytest.skip(f'need nghttp2 >= 1.50.0')
+        conf = H2Conf(env)
+        conf.add([
+            "H2HeaderStrictness rfc9113"
+        ])
+        conf.add_vhost_test1()
+        conf.install()
+        assert env.apache_restart() == 0
+        url = env.mkurl("https", "test1", "/")
+        r = env.curl_get(url, options=['-H', 'trailing-space: must not  '])
+        assert r.exit_code != 0, f'curl output: {r.stderr}'
+        r = env.curl_get(url, options=['-H', 'trailing-space: must not\t'])
+        assert r.exit_code != 0, f'curl output: {r.stderr}'
+

Modified: httpd/httpd/trunk/test/modules/http2/test_401_early_hints.py
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/test/modules/http2/test_401_early_hints.py?rev=1904522&r1=1904521&r2=1904522&view=diff
==============================================================================
--- httpd/httpd/trunk/test/modules/http2/test_401_early_hints.py (original)
+++ httpd/httpd/trunk/test/modules/http2/test_401_early_hints.py Tue Oct 11 14:54:08 2022
@@ -26,7 +26,7 @@ class TestEarlyHints:
         assert env.apache_restart() == 0
 
     # H2EarlyHints enabled in general, check that it works for H2PushResource
-    def test_h2_401_31(self, env):
+    def test_h2_401_31(self, env, repeat):
         url = env.mkurl("https", "hints", "/006-hints.html")
         r = env.nghttp().get(url)
         assert r.response["status"] == 200
@@ -38,7 +38,7 @@ class TestEarlyHints:
         assert early["header"]["link"]
 
     # H2EarlyHints enabled in general, but does not trigger on added response headers
-    def test_h2_401_32(self, env):
+    def test_h2_401_32(self, env, repeat):
         url = env.mkurl("https", "hints", "/006-nohints.html")
         r = env.nghttp().get(url)
         assert r.response["status"] == 200

Modified: httpd/httpd/trunk/test/modules/http2/test_500_proxy.py
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/test/modules/http2/test_500_proxy.py?rev=1904522&r1=1904521&r2=1904522&view=diff
==============================================================================
--- httpd/httpd/trunk/test/modules/http2/test_500_proxy.py (original)
+++ httpd/httpd/trunk/test/modules/http2/test_500_proxy.py Tue Oct 11 14:54:08 2022
@@ -126,3 +126,28 @@ class TestProxy:
     def test_h2_500_24(self, env):
         for i in range(100):
             self.nghttp_upload_stat(env, "data-1k", ["--no-content-length"])
+
+    # lets do some error tests
+    def test_h2_500_30(self, env):
+        url = env.mkurl("https", "cgi", "/proxy/h2test/error?status=500")
+        r = env.curl_get(url)
+        assert r.exit_code == 0, r
+        assert r.response['status'] == 500
+        url = env.mkurl("https", "cgi", "/proxy/h2test/error?error=timeout")
+        r = env.curl_get(url)
+        assert r.exit_code == 0, r
+        assert r.response['status'] == 408
+
+    # produce an error during response body
+    def test_h2_500_31(self, env, repeat):
+        pytest.skip("needs fix in core protocol handling")
+        url = env.mkurl("https", "cgi", "/proxy/h2test/error?body_error=timeout")
+        r = env.curl_get(url)
+        assert r.exit_code != 0, r
+
+    # produce an error, fail to generate an error bucket
+    def test_h2_500_32(self, env, repeat):
+        pytest.skip("needs fix in core protocol handling")
+        url = env.mkurl("https", "cgi", "/proxy/h2test/error?body_error=timeout&error_bucket=0")
+        r = env.curl_get(url)
+        assert r.exit_code != 0, r

Modified: httpd/httpd/trunk/test/modules/http2/test_600_h2proxy.py
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/test/modules/http2/test_600_h2proxy.py?rev=1904522&r1=1904521&r2=1904522&view=diff
==============================================================================
--- httpd/httpd/trunk/test/modules/http2/test_600_h2proxy.py (original)
+++ httpd/httpd/trunk/test/modules/http2/test_600_h2proxy.py Tue Oct 11 14:54:08 2022
@@ -23,7 +23,7 @@ class TestH2Proxy:
         assert r.response["json"]["ssl_protocol"] != ""
         assert r.response["json"]["h2"] == "on"
         assert r.response["json"]["h2push"] == "off"
-        assert r.response["json"]["x_host"] == f"cgi.{env.http_tld}:{env.https_port}"
+        assert r.response["json"]["host"] == f"cgi.{env.http_tld}:{env.https_port}"
 
     def test_h2_600_02(self, env):
         conf = H2Conf(env, extras={
@@ -42,7 +42,8 @@ class TestH2Proxy:
         assert r.response["json"]["protocol"] == "HTTP/2.0"
         assert r.response["json"]["https"] == ""
         # the proxied backend sees Host header as passed on front
-        assert r.response["json"]["x_host"] == f"cgi.{env.http_tld}:{env.https_port}"
+        assert r.response["json"]["host"] == f"cgi.{env.http_tld}:{env.https_port}"
+        assert r.response["json"]["h2_original_host"] == ""
 
     def test_h2_600_03(self, env):
         conf = H2Conf(env, extras={
@@ -61,4 +62,116 @@ class TestH2Proxy:
         assert r.response["json"]["protocol"] == "HTTP/2.0"
         assert r.response["json"]["https"] == ""
         # the proxied backend sees Host as using in connecting to it
-        assert r.response["json"]["x_host"] == f"127.0.0.1:{env.http_port}"
+        assert r.response["json"]["host"] == f"127.0.0.1:{env.http_port}"
+        assert r.response["json"]["h2_original_host"] == ""
+
+    # check that connection reuse actually happens as configured
+    @pytest.mark.parametrize("enable_reuse", [ "on", "off" ])
+    def test_h2_600_04(self, env, enable_reuse):
+        conf = H2Conf(env, extras={
+            f'cgi.{env.http_tld}': [
+                f"ProxyPassMatch ^/h2proxy/([0-9]+)/(.*)$ "
+                f"  h2c://127.0.0.1:$1/$2 enablereuse={enable_reuse} keepalive=on",
+            ]
+        })
+        conf.add_vhost_cgi()
+        conf.install()
+        assert env.apache_restart() == 0
+        url = env.mkurl("https", "cgi", f"/h2proxy/{env.http_port}/hello.py")
+        r = env.curl_get(url, 5)
+        assert r.response["status"] == 200
+        assert r.json["h2_stream_id"] == "1"
+        # httpd 2.5.0 disables reuse, not matter the config
+        if enable_reuse == "on" and not env.httpd_is_at_least("2.5.0"):
+            # reuse is not guarantueed for each request, but we expect some
+            # to do it and run on a h2 stream id > 1
+            reused = False
+            for _ in range(10):
+                r = env.curl_get(url, 5)
+                assert r.response["status"] == 200
+                if int(r.json["h2_stream_id"]) > 1:
+                    reused = True
+                    break
+            assert reused
+        else:
+            r = env.curl_get(url, 5)
+            assert r.response["status"] == 200
+            assert r.json["h2_stream_id"] == "1"
+
+    # do some flexible setup from #235 to proper connection selection
+    @pytest.mark.parametrize("enable_reuse", [ "on", "off" ])
+    def test_h2_600_05(self, env, enable_reuse):
+        conf = H2Conf(env, extras={
+            f'cgi.{env.http_tld}': [
+                f"ProxyPassMatch ^/h2proxy/([0-9]+)/(.*)$ "
+                f"  h2c://127.0.0.1:$1/$2 enablereuse={enable_reuse} keepalive=on",
+            ]
+        })
+        conf.add_vhost_cgi()
+        conf.add([
+            f'Listen {env.http_port2}',
+            'UseCanonicalName On',
+            'UseCanonicalPhysicalPort On'
+        ])
+        conf.start_vhost(domains=[f'cgi.{env.http_tld}'],
+                         port=5004, doc_root="htdocs/cgi")
+        conf.add("AddHandler cgi-script .py")
+        conf.end_vhost()
+        conf.install()
+        assert env.apache_restart() == 0
+        url = env.mkurl("https", "cgi", f"/h2proxy/{env.http_port}/hello.py")
+        r = env.curl_get(url, 5)
+        assert r.response["status"] == 200
+        assert int(r.json["port"]) == env.http_port
+        # going to another backend port must create a new connection and
+        # we should see stream id one again
+        url = env.mkurl("https", "cgi", f"/h2proxy/{env.http_port2}/hello.py")
+        r = env.curl_get(url, 5)
+        assert r.response["status"] == 200
+        exp_port = env.http_port if enable_reuse == "on" \
+                                    and not env.httpd_is_at_least("2.5.0")\
+            else env.http_port2
+        assert int(r.json["port"]) == exp_port
+
+    # lets do some error tests
+    def test_h2_600_30(self, env):
+        conf = H2Conf(env)
+        conf.add_vhost_cgi(h2proxy_self=True)
+        conf.install()
+        assert env.apache_restart() == 0
+        url = env.mkurl("https", "cgi", "/h2proxy/h2test/error?status=500")
+        r = env.curl_get(url)
+        assert r.exit_code == 0, r
+        assert r.response['status'] == 500
+        url = env.mkurl("https", "cgi", "/h2proxy/h2test/error?error=timeout")
+        r = env.curl_get(url)
+        assert r.exit_code == 0, r
+        assert r.response['status'] == 408
+
+    # produce an error during response body
+    def test_h2_600_31(self, env, repeat):
+        pytest.skip("needs fix in core protocol handling")
+        conf = H2Conf(env)
+        conf.add_vhost_cgi(h2proxy_self=True)
+        conf.install()
+        assert env.apache_restart() == 0
+        url = env.mkurl("https", "cgi", "/h2proxy/h2test/error?body_error=timeout")
+        r = env.curl_get(url)
+        # depending on when the error is detect in proxying, if may RST the
+        # stream (exit_code != 0) or give a 503 response.
+        if r.exit_code == 0:
+            assert r.response['status'] == 503
+
+    # produce an error, fail to generate an error bucket
+    def test_h2_600_32(self, env, repeat):
+        pytest.skip("needs fix in core protocol handling")
+        conf = H2Conf(env)
+        conf.add_vhost_cgi(h2proxy_self=True)
+        conf.install()
+        assert env.apache_restart() == 0
+        url = env.mkurl("https", "cgi", "/h2proxy/h2test/error?body_error=timeout&error_bucket=0")
+        r = env.curl_get(url)
+        # depending on when the error is detect in proxying, if may RST the
+        # stream (exit_code != 0) or give a 503 response.
+        if r.exit_code == 0:
+            assert r.response['status'] == 503

Modified: httpd/httpd/trunk/test/pyhttpd/conf.py
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/test/pyhttpd/conf.py?rev=1904522&r1=1904521&r2=1904522&view=diff
==============================================================================
--- httpd/httpd/trunk/test/pyhttpd/conf.py (original)
+++ httpd/httpd/trunk/test/pyhttpd/conf.py Tue Oct 11 14:54:08 2022
@@ -157,8 +157,6 @@ class HttpdConf(object):
         self.start_vhost(domains=[domain, f"cgi-alias.{self.env.http_tld}"],
                          port=self.env.https_port, doc_root="htdocs/cgi")
         self.add_proxies("cgi", proxy_self=proxy_self, h2proxy_self=h2proxy_self)
-        if domain in self._extras:
-            self.add(self._extras[domain])
         self.end_vhost()
         self.start_vhost(domains=[domain, f"cgi-alias.{self.env.http_tld}"],
                          port=self.env.http_port, doc_root="htdocs/cgi")

Modified: httpd/httpd/trunk/test/pyhttpd/config.ini.in
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/test/pyhttpd/config.ini.in?rev=1904522&r1=1904521&r2=1904522&view=diff
==============================================================================
--- httpd/httpd/trunk/test/pyhttpd/config.ini.in (original)
+++ httpd/httpd/trunk/test/pyhttpd/config.ini.in Tue Oct 11 14:54:08 2022
@@ -25,6 +25,7 @@ gen_dir = @abs_srcdir@/../gen
 http_port = 5002
 https_port = 5001
 proxy_port = 5003
+http_port2 = 5004
 http_tld = tests.httpd.apache.org
 test_dir = @abs_srcdir@
 test_src_dir = @abs_srcdir@

Modified: httpd/httpd/trunk/test/pyhttpd/env.py
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/test/pyhttpd/env.py?rev=1904522&r1=1904521&r2=1904522&view=diff
==============================================================================
--- httpd/httpd/trunk/test/pyhttpd/env.py (original)
+++ httpd/httpd/trunk/test/pyhttpd/env.py Tue Oct 11 14:54:08 2022
@@ -244,6 +244,7 @@ class HttpdTestEnv:
             self._h2load = 'h2load'
 
         self._http_port = int(self.config.get('test', 'http_port'))
+        self._http_port2 = int(self.config.get('test', 'http_port2'))
         self._https_port = int(self.config.get('test', 'https_port'))
         self._proxy_port = int(self.config.get('test', 'proxy_port'))
         self._http_tld = self.config.get('test', 'http_tld')
@@ -346,6 +347,10 @@ class HttpdTestEnv:
         return self._http_port
 
     @property
+    def http_port2(self) -> int:
+        return self._http_port2
+
+    @property
     def https_port(self) -> int:
         return self._https_port
 

Modified: httpd/httpd/trunk/test/pyhttpd/nghttp.py
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/test/pyhttpd/nghttp.py?rev=1904522&r1=1904521&r2=1904522&view=diff
==============================================================================
--- httpd/httpd/trunk/test/pyhttpd/nghttp.py (original)
+++ httpd/httpd/trunk/test/pyhttpd/nghttp.py Tue Oct 11 14:54:08 2022
@@ -121,7 +121,8 @@ class Nghttp:
                                 prev["previous"] = response["previous"]
                             response["previous"] = prev
                     response[hkey] = s["header"]
-                    s["header"] = {} 
+                    s["header"] = {}
+                    body = ''
                 continue
             
             m = re.match(r'(.*)\[.*] recv DATA frame <length=(\d+), .*stream_id=(\d+)>', l)

Modified: httpd/httpd/trunk/test/pyhttpd/result.py
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/test/pyhttpd/result.py?rev=1904522&r1=1904521&r2=1904522&view=diff
==============================================================================
--- httpd/httpd/trunk/test/pyhttpd/result.py (original)
+++ httpd/httpd/trunk/test/pyhttpd/result.py Tue Oct 11 14:54:08 2022
@@ -9,21 +9,21 @@ class ExecResult:
                  stdout: bytes, stderr: bytes = None, duration: timedelta = None):
         self._args = args
         self._exit_code = exit_code
-        self._raw = stdout if stdout else b''
-        self._stdout = stdout.decode() if stdout is not None else ""
-        self._stderr = stderr.decode() if stderr is not None else ""
+        self._stdout = stdout if stdout is not None else b''
+        self._stderr = stderr if stderr is not None else b''
         self._duration = duration if duration is not None else timedelta()
         self._response = None
         self._results = {}
         self._assets = []
         # noinspection PyBroadException
         try:
-            self._json_out = json.loads(self._stdout)
+            out = self._stdout.decode()
+            self._json_out = json.loads(out)
         except:
             self._json_out = None
 
     def __repr__(self):
-        return f"ExecResult[code={self.exit_code}, args={self._args}, stdout={self.stdout}, stderr={self.stderr}]"
+        return f"ExecResult[code={self.exit_code}, args={self._args}, stdout={self._stdout}, stderr={self._stderr}]"
 
     @property
     def exit_code(self) -> int:
@@ -35,11 +35,11 @@ class ExecResult:
 
     @property
     def outraw(self) -> bytes:
-        return self._raw
+        return self._stdout
 
     @property
     def stdout(self) -> str:
-        return self._stdout
+        return self._stdout.decode()
 
     @property
     def json(self) -> Optional[Dict]:
@@ -48,7 +48,7 @@ class ExecResult:
 
     @property
     def stderr(self) -> str:
-        return self._stderr
+        return self._stderr.decode()
 
     @property
     def duration(self) -> timedelta: