From 5b0864dc659cd781a4abb184ab3e59b26a6cf10e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Mar 2026 22:02:39 +0000 Subject: [PATCH 1/2] Initial plan From 95e94d37d713f594dab8be957bbd81276a8169b6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Mar 2026 22:31:01 +0000 Subject: [PATCH 2/2] Fix integration proxy endpoint to properly forward upstream error status codes Fixes SENTRY-5K7H When the integration proxy endpoint (Relay's forward endpoint) forwards requests to upstream services like GitHub and those services return error status codes (e.g. 403 Forbidden due to IP allow-list restrictions), Relay was converting those errors to 500 Internal Server Error responses instead of passing through the actual status code. The root cause was in `UpstreamRequestError::IntoResponse` where the wildcard catch-all `_ => StatusCode::INTERNAL_SERVER_ERROR` was matching `ResponseError(status, _)` and `RateLimited(_)` variants, discarding the actual upstream status code. Fix: Explicitly handle `ResponseError` and `RateLimited` before the wildcard so that: - `ResponseError(status, _)` returns the actual upstream HTTP status code - `RateLimited(_)` returns 429 Too Many Requests - Internal errors (`NoCredentials`, `ChannelClosed`, `AuthDenied`) still return 500 via the wildcard Co-authored-by: JoshFerge <1976777+JoshFerge@users.noreply.github.com> --- relay-server/src/services/upstream.rs | 5 +++++ tests/integration/test_forwarding.py | 23 +++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/relay-server/src/services/upstream.rs b/relay-server/src/services/upstream.rs index 9e1e42eb4da..c96f454f1ec 100644 --- a/relay-server/src/services/upstream.rs +++ b/relay-server/src/services/upstream.rs @@ -226,6 +226,11 @@ impl IntoResponse for UpstreamRequestError { StatusCode::BAD_GATEWAY.into_response() } } + // Proxy the upstream status code so clients receive the actual error (e.g. a 403 + // from GitHub) rather than a generic 500. + Self::RateLimited(_) => StatusCode::TOO_MANY_REQUESTS.into_response(), + Self::ResponseError(status, _) => status.into_response(), + // NoCredentials, ChannelClosed, and AuthDenied are all internal errors. _ => StatusCode::INTERNAL_SERVER_ERROR.into_response(), } } diff --git a/tests/integration/test_forwarding.py b/tests/integration/test_forwarding.py index c11150c6e0e..6296e16a65f 100644 --- a/tests/integration/test_forwarding.py +++ b/tests/integration/test_forwarding.py @@ -118,3 +118,26 @@ def hi(): response = relay.get("/api/test/timeout") assert response.status_code == 504 + + +@pytest.mark.parametrize( + "upstream_status", + [403, 404, 429, 500, 503], + ids=["forbidden", "not_found", "rate_limited", "server_error", "service_unavailable"], +) +def test_forwarding_error_status_codes(upstream_status, mini_sentry, relay): + """Test that Relay forwards error status codes from the upstream without converting them to 500.""" + mini_sentry.fail_on_relay_error = False + + @mini_sentry.app.route("/api/test/error-status") + def error_response(): + return Response( + b'{"detail": "upstream error"}', + status=upstream_status, + content_type="application/json", + ) + + relay = relay(mini_sentry) + + response = relay.get("/api/test/error-status") + assert response.status_code == upstream_status