From f8a3cd35b80224a6a1910ee38697a41022292781 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Houpert?= <10154151+lhoupert@users.noreply.github.com> Date: Fri, 18 Sep 2026 14:36:21 +0100 Subject: [PATCH 1/2] perf: update reverse_proxy.py to return correct status code for upstream request timeout --- src/stac_auth_proxy/handlers/reverse_proxy.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/stac_auth_proxy/handlers/reverse_proxy.py b/src/stac_auth_proxy/handlers/reverse_proxy.py index 203d103d..40863d8c 100644 --- a/src/stac_auth_proxy/handlers/reverse_proxy.py +++ b/src/stac_auth_proxy/handlers/reverse_proxy.py @@ -103,7 +103,12 @@ async def proxy_request(self, request: Request) -> Response: logger.debug(f"Proxying request to {rp_req.url}") start_time = time.perf_counter() - rp_resp = await self.client.send(rp_req, stream=True) + try: + rp_resp = await self.client.send(rp_req, stream=True) + except httpx.TimeoutException: + return Response(status_code=504, content=b"Upstream timed out") + except httpx.ConnectError: + return Response(status_code=502, content=b"Upstream unreachable") proxy_time = time.perf_counter() - start_time rp_resp.headers["Server-Timing"] = build_server_timing_header( rp_resp.headers.get("Server-Timing"), From 64a9120534775f12377506472df757e691f91b3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Houpert?= <10154151+lhoupert@users.noreply.github.com> Date: Mon, 21 Sep 2026 12:02:22 +0100 Subject: [PATCH 2/2] test: cover upstream timeout and connect-error handling in reverse proxy The 504/502 branches added for upstream transport failures had no test coverage, failing codecov/patch and codecov/project. Drive them via an httpx.MockTransport that raises, and give the mock request helper a receive channel so proxy_request can stream its body. Co-Authored-By: Claude Opus 5 (1M context) --- tests/test_reverse_proxy.py | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/tests/test_reverse_proxy.py b/tests/test_reverse_proxy.py index 6dd525a3..b11b8cd6 100644 --- a/tests/test_reverse_proxy.py +++ b/tests/test_reverse_proxy.py @@ -1,11 +1,17 @@ """Tests for the reverse proxy handler's header functionality.""" +import httpx import pytest from fastapi import Request from stac_auth_proxy.handlers.reverse_proxy import ReverseProxyHandler +async def empty_body(): + """Receive channel that yields an empty request body.""" + return {"type": "http.request", "body": b"", "more_body": False} + + def create_request(scope_overrides=None, headers=None): """Create a mock FastAPI request with custom scope and headers.""" default_scope = { @@ -25,7 +31,7 @@ def create_request(scope_overrides=None, headers=None): if headers: default_scope["headers"] = headers - return Request(default_scope) + return Request(default_scope, receive=empty_body) @pytest.fixture @@ -310,3 +316,27 @@ async def test_x_forwarded_port_in_forwarded_header(legacy_headers): # Check that the x-forwarded-port header is preserved assert result_headers["X-Forwarded-Port"] == "443" + + +@pytest.mark.parametrize( + "exception,expected_status", + [ + (httpx.ConnectTimeout("Timed out"), 504), + (httpx.ConnectError("Connection refused"), 502), + ], +) +async def test_upstream_transport_errors(exception, expected_status): + """Transport failures become gateway responses rather than unhandled errors.""" + + def raise_error(request): + raise exception + + handler = ReverseProxyHandler( + upstream="http://upstream-api.com", + client=httpx.AsyncClient( + base_url="http://upstream-api.com", + transport=httpx.MockTransport(raise_error), + ), + ) + response = await handler.proxy_request(create_request()) + assert response.status_code == expected_status