From: Jim Jagielski Date: Wed, 3 Jun 2026 21:25:44 +0000 (+0000) Subject: test/pytest_suite: fix more tests broken by httpx auto-decompression X-Git-Url: http://git.ipfire.org/gitweb/index.cgi?a=commitdiff_plain;h=5079dfa77f9e5ae1a029bb78199ed250d38ab42e;p=thirdparty%2Fapache%2Fhttpd.git test/pytest_suite: fix more tests broken by httpx auto-decompression httpx transparently inflates gzip/deflate response bodies (it keeps the Content-Encoding header but .content/.text are the decoded plaintext). Three more tests asserted on the body of a compressed response and so were wrong: - pr43939 / pr17629: re-POSTed r.content (decoded plaintext, not the gzip stream) through the inflate input filter -- same round-trip bug as pr49328/deflate. - reflector: 'assert r.text != payload' could never hold, since httpx decoded the gzip body straight back to the payload. Generalise the GET_RAW helper into a raw_response(method, path, ...) that streams the response, reads iter_raw() (undecoded bytes), and stashes them on .raw_content while leaving .status_code/.headers intact -- so tests can assert on both the raw body and the headers. GET_RAW now delegates to it; update the three tests to use raw_response/.raw_content. The length-based tests (rwrite, passbrigade, getfile, byterange7) are unaffected: they measure len(.content) after decoding, which equals the original length regardless of transfer compression. git-svn-id: https://svn.apache.org/repos/asf/httpd/httpd/trunk@1934950 13f79535-47bb-0310-9956-ffa450edef68 --- diff --git a/test/pytest_suite/apache_pytest/client.py b/test/pytest_suite/apache_pytest/client.py index 572b448ba6..4385ed2801 100644 --- a/test/pytest_suite/apache_pytest/client.py +++ b/test/pytest_suite/apache_pytest/client.py @@ -272,33 +272,37 @@ class TestClient: def GET(self, path: str, **kwargs: object) -> httpx.Response: return self._request("GET", path, **kwargs) - def _raw_body( + def raw_response( self, method: str, path: str, *, cert: str | None = None, **kwargs: object, - ) -> bytes: - """Return the response body WITHOUT content-decoding. + ) -> httpx.Response: + """Like :meth:`_request` but WITHOUT content-decoding the body. httpx transparently inflates gzip/deflate responses, so ``.content`` is - the decoded plaintext. The mod_deflate round-trip tests need the raw - compressed bytes (to re-POST them through the inflate input filter), so - stream the response and read ``iter_raw()``, which yields the bytes as - they came off the wire -- the analog of LWP not auto-decoding. + the decoded plaintext (while ``Content-Encoding`` is left in place). + Tests that need the bytes exactly as they came off the wire -- e.g. the + mod_deflate round-trips that re-POST the gzip through an inflate filter, + or mod_reflector asserting the body was actually transformed -- can't use + that. Stream the response, read ``iter_raw()`` (the undecoded bytes, the + analog of LWP not auto-decoding), and stash them on ``.raw_content`` so + callers still see ``.status_code`` and ``.headers``. """ client = self._client_for(cert) if cert is not None else self._client request = client.build_request(method, self._url(path), **kwargs) # type: ignore[arg-type] response = client.send(request, stream=True) try: - return b"".join(response.iter_raw()) + response.raw_content = b"".join(response.iter_raw()) # type: ignore[attr-defined] finally: response.close() + return response def GET_RAW(self, path: str, **kwargs: object) -> bytes: """GET ``path`` and return the raw, undecoded response body bytes.""" - return self._raw_body("GET", path, **kwargs) + return self.raw_response("GET", path, **kwargs).raw_content # type: ignore[attr-defined] def HEAD(self, path: str, **kwargs: object) -> httpx.Response: return self._request("HEAD", path, **kwargs) diff --git a/test/pytest_suite/tests/t/apache/test_pr17629.py b/test/pytest_suite/tests/t/apache/test_pr17629.py index 13c714b92f..368f1b68d7 100644 --- a/test/pytest_suite/tests/t/apache/test_pr17629.py +++ b/test/pytest_suite/tests/t/apache/test_pr17629.py @@ -21,13 +21,15 @@ def test_pr17629(http): content = http.GET_BODY(URI) assert t_cmp(content, EXPECTED) - r = http.GET(URI, headers={"Accept-Encoding": "gzip"}) + # raw_response keeps the gzip stream undecoded (httpx would auto-decompress + # .content) so we can re-POST it through the inflate input filter. + r = http.raw_response("GET", URI, headers={"Accept-Encoding": "gzip"}) assert t_cmp(r.status_code, 200) renc = r.headers.get("Content-Encoding", "") assert t_cmp(renc, "gzip"), "response was gzipped" deflated = http.POST_BODY( - INFLATOR, content=r.content, headers={"Content-Encoding": "gzip"} + INFLATOR, content=r.raw_content, headers={"Content-Encoding": "gzip"} ) assert t_cmp(deflated, EXPECTED) diff --git a/test/pytest_suite/tests/t/apache/test_pr43939.py b/test/pytest_suite/tests/t/apache/test_pr43939.py index 7228e83cac..bb72cb0837 100644 --- a/test/pytest_suite/tests/t/apache/test_pr43939.py +++ b/test/pytest_suite/tests/t/apache/test_pr43939.py @@ -21,13 +21,15 @@ def test_pr43939(http): content = http.GET_BODY(URI) assert t_cmp(content, EXPECTED) - r = http.GET(URI, headers={"Accept-Encoding": "gzip"}) + # raw_response keeps the gzip stream undecoded (httpx would auto-decompress + # .content) so we can re-POST it through the inflate input filter. + r = http.raw_response("GET", URI, headers={"Accept-Encoding": "gzip"}) assert t_cmp(r.status_code, 200) renc = r.headers.get("Content-Encoding", "") assert t_cmp(renc, "gzip"), "response was gzipped" deflated = http.POST_BODY( - INFLATOR, content=r.content, headers={"Content-Encoding": "gzip"} + INFLATOR, content=r.raw_content, headers={"Content-Encoding": "gzip"} ) assert t_cmp(deflated, EXPECTED) diff --git a/test/pytest_suite/tests/t/modules/test_reflector.py b/test/pytest_suite/tests/t/modules/test_reflector.py index 66d0645bf7..a98ed0287a 100644 --- a/test/pytest_suite/tests/t/modules/test_reflector.py +++ b/test/pytest_suite/tests/t/modules/test_reflector.py @@ -29,18 +29,22 @@ HEADERS = { @need_module("mod_reflector", "mod_deflate") @pytest.mark.parametrize("url,payload", TESTCASES, ids=lambda v: str(v)) def test_reflector(http, url, payload): - r = http.POST(url, content=payload, headers=HEADERS) + # raw_response keeps the body undecoded: the deflate case must observe that + # the gzip body differs from the payload, but httpx would auto-decompress + # .text/.content back to the payload and mask the transformation. + r = http.raw_response("POST", url, content=payload, headers=HEADERS) + body = r.raw_content.decode("latin-1") assert t_cmp(r.status_code, 200), "Checking return code is '200'" if "_nodeflate" in url: # With no filter, we should receive what we have sent. - assert t_cmp(r.text, payload) + assert t_cmp(body, payload) assert t_cmp(r.headers.get("Content-Encoding"), None), \ "'Content-Encoding' has not been added because there was no filter" else: # With DEFLATE, input was updated and 'Content-Encoding' added. - assert r.text != payload + assert body != payload assert t_cmp(r.headers.get("Content-Encoding"), "gzip"), \ "'Content-Encoding' has been added by the DEFLATE filter"