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