Skip to content

fix(responses): surface upstream error status on get instead of 500 - #32287

Merged
mateo-berri merged 2 commits into
litellm_internal_stagingfrom
litellm_fix_responses_get_error_body
Jul 7, 2026
Merged

fix(responses): surface upstream error status on get instead of 500#32287
mateo-berri merged 2 commits into
litellm_internal_stagingfrom
litellm_fix_responses_get_error_body

Conversation

@mateo-berri

@mateo-berri mateo-berri commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Relevant issues

Linear ticket

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have added meaningful tests
  • My PR passes all CI/CD checks (e.g., lint, format, unit tests)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have requested a Greptile review by commenting @greptileai and received a Confidence Score of at least 4/5 before requesting a maintainer review

Delays in PR merge?

If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).

Screenshots / Proof of Fix

Live proxy against a real Azure OpenAI deployment (azure/gpt-4o, api_version 2025-03-01-preview), run with .venv/bin/python litellm/proxy/proxy_cli.py --config config.yaml --port 29764 --detailed_debug

Before, on litellm_internal_staging @ 7f991481cc069d7a069a8a50c140bcfaec9a4e6c. Create a non-retrievable response, then GET it

curl -s http://localhost:29764/v1/responses \
  -H "Authorization: Bearer sk-fix4-local" -H "Content-Type: application/json" \
  -d '{"model":"azure-responses-test","input":"ping","store":false}'
# 200, id resp_LOQEeaPx46R3...

curl -s -w "\nHTTP_STATUS:%{http_code}\n" http://localhost:29764/v1/responses/resp_LOQEeaPx46R3... \
  -H "Authorization: Bearer sk-fix4-local"
{"error":{"message":"litellm.APIConnectionError: AzureException APIConnectionError - 3 validation errors for ResponsesAPIResponse\nid\n  Field required [type=missing, input_value={'error': {'message': \"Re...m': None, 'code': None}}, input_type=dict]\n ...","type":null,"param":null,"code":"500"}}
HTTP_STATUS:500

Same on a deleted id: POST with "store": true, DELETE it (200 response.deleted), then GET the deleted id returns the identical 500 pydantic ValidationError. Control: POST stored + GET returns 200

After, on fd9d0cf3f6ff9e718a5e172c17ad485f536a1fa2. Same two error curls now surface the real upstream status and message

curl -s -w "\nHTTP_STATUS:%{http_code}\n" http://localhost:29764/v1/responses/resp_04fd61c97344... \
  -H "Authorization: Bearer sk-fix4-local"
{"error":{"message":"litellm.NotFoundError: AzureException NotFoundError - {\n  \"error\": {\n    \"message\": \"Response with id 'resp_04fd61c97344f4f8016a4c26fa6fcc819391ced2af096216ae' not found.\",\n    \"type\": \"invalid_request_error\",\n    \"param\": null,\n    \"code\": null\n  }\n} ...","type":null,"param":null,"code":"404"}}
HTTP_STATUS:404

GET on a deleted id likewise returns 404 with Azure's "Response with id ... not found." message. The stored control still returns 200 with the full response, and DELETE still returns 200

Type

🐛 Bug Fix

Changes

GET /v1/responses/{id} returned a 500 APIConnectionError whenever the upstream provider returned an error (response created with "store": false, or id already deleted). Root cause: HTTPHandler.get / AsyncHTTPHandler.get never call raise_for_status() (unlike post and delete), so the upstream error JSON flowed into transform_get_response_api_response, which force-parsed it into the ResponsesAPIResponse pydantic model and blew up with ValidationError: 3 validation errors ... id/created_at/output Field required

llm_http_handler.py: the four responses GET call sites (get_responses, async_get_responses, list_responses_input_items, async_list_responses_input_items) now call response.raise_for_status() inside the existing try, so upstream errors route through _handle_error and the provider error class with their real status and body, matching what create/cancel (POST) and delete already do

exception_mapping_utils.py: the azure mapper had no 404 branch in its status ladder, and its "invalid_request_error" in error_str string match ran before any status check, downgrading Azure's 404 (which carries "type": "invalid_request_error") to a 400. Added the 404 branch raising NotFoundError and gated the string match to status 400 or no status, so real statuses survive the mapping

Regression tests in the mapped test files: tests/test_litellm/llms/custom_httpx/test_llm_http_handler.py covers async GET, sync GET, and input items surfacing a 404 NotFoundError (dependency-injected client backed by httpx.MockTransport), and tests/test_litellm/litellm_core_utils/test_exception_mapping_utils.py pins azure 404 + invalid_request_error mapping to NotFoundError. All four tests fail on the pre-fix code and pass with the fix

@mateo-berri

Copy link
Copy Markdown
Contributor Author

@greptileai

@greptile-apps

greptile-apps Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes GET /v1/responses/{id} (and GET .../input_items) surfacing a 500 APIConnectionError instead of the real upstream error. The root cause was two missing pieces: GET paths never called raise_for_status(), and the Azure exception mapper unconditionally matched "invalid_request_error" (present in Azure 404 bodies) before checking the HTTP status code.

  • llm_http_handler.py: response.raise_for_status() added to all four GET call sites (sync/async get_responses and list_responses_input_items), matching the existing pattern already used by the POST/DELETE paths; also demotes verbose_logger.exception to verbose_logger.debug for expected upstream errors.
  • exception_mapping_utils.py: The "invalid_request_error" string-match branch is now gated to status_code in (None, 400), and a dedicated 404 → NotFoundError branch is added to the Azure status-code ladder so real 404s are no longer silently downgraded to BadRequestError.
  • Tests: All four changed GET paths are covered by new mock-transport tests, plus a direct unit test for the Azure 404 exception-mapping fix; no real network calls are made.

Confidence Score: 5/5

Safe to merge — changes are narrowly scoped to four GET call sites and one Azure exception-mapping branch, with mock-transport tests covering each modified path.

The fix correctly applies raise_for_status() to align GET with the existing post/delete pattern, and the exception-mapping change is carefully gated so only the 400/None ambiguous case falls back to string-matching while real status codes (404, 429, 500 etc.) go through the status-code ladder. All four call sites have regression tests. No existing behavior is broken for successful (2xx) responses.

No files require special attention.

Important Files Changed

Filename Overview
litellm/llms/custom_httpx/llm_http_handler.py Adds response.raise_for_status() to all four GET call sites (sync/async get_responses and list_responses_input_items), matching the existing post/delete pattern; also correctly demotes the async_get_responses log from exception to debug.
litellm/litellm_core_utils/exception_mapping_utils.py Gates the "invalid_request_error" string-match branch to status_code in (None, 400) and adds a dedicated 404 branch in the Azure status-code ladder, fixing Azure 404s being incorrectly downgraded to BadRequestError.
tests/test_litellm/llms/custom_httpx/test_llm_http_handler.py Adds mock-transport tests covering all four changed GET paths (async and sync get_responses, async and sync list_input_items); uses httpx.MockTransport to avoid real network calls, consistent with repo test rules.
tests/test_litellm/litellm_core_utils/test_exception_mapping_utils.py Adds a regression test pinning Azure 404 + invalid_request_error body to NotFoundError, exercising the updated _map_azure_exception branch directly.

Reviews (3): Last reviewed commit: "fix(responses): demote expected 4xx get ..." | Re-trigger Greptile

Comment thread litellm/llms/custom_httpx/llm_http_handler.py Outdated
Comment thread tests/test_litellm/llms/custom_httpx/test_llm_http_handler.py
@greptile-apps

greptile-apps Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes GET /v1/responses/{id} returning a 500 APIConnectionError whenever the upstream provider returned an error, by adding response.raise_for_status() to all four GET call sites in llm_http_handler.py so HTTP errors are routed through _handle_error instead of falling through to the pydantic transformer. It also fixes the Azure exception mapper, which was downgrading real 404 responses to BadRequestError because their "invalid_request_error" type field hit a string-match branch before the status-code ladder.

  • llm_http_handler.py: four raise_for_status() calls added to get_responses (sync + async) and list_responses_input_items (sync + async); upstream HTTP errors now follow the same _handle_error path as POST/DELETE already did.
  • exception_mapping_utils.py: added a 404 branch to the Azure status-code ladder and gated the "invalid_request_error" string match to status 400 or unset, so 404s with that error type correctly raise NotFoundError.
  • New mock-transport unit tests cover async GET, sync GET, and async list-input-items; the sync list_responses_input_items path is the only one without a dedicated new test.

Confidence Score: 4/5

The change is narrow, well-tested, and fixes a real regression without touching any auth or critical-path logic.

Both changes are straightforward: adding raise_for_status() to GET handlers and inserting a 404 branch in the Azure exception mapper. The logic is correct, the mock tests verify the end-to-end mapping, and the fix is isolated to the Responses API GET paths. The only friction is that async_get_responses still logs expected 4xx errors at exception level, which will add stack-trace noise to production logs for normal client errors like 'response not found'.

llm_http_handler.py around the async_get_responses exception handler — the pre-existing verbose_logger.exception call will now emit stack traces for ordinary 404 responses.

Important Files Changed

Filename Overview
litellm/llms/custom_httpx/llm_http_handler.py Adds response.raise_for_status() to four GET call sites so upstream HTTP errors route through _handle_error instead of reaching the pydantic transformer and exploding as ValidationError; async get_responses will now also emit a verbose_logger.exception stack trace for expected 4xx errors.
litellm/litellm_core_utils/exception_mapping_utils.py Adds a 404 branch to the Azure status-code ladder and gates the "invalid_request_error" string match to status 400/None, preventing Azure 404s (which carry "type": "invalid_request_error") from being downgraded to BadRequestError.
tests/test_litellm/llms/custom_httpx/test_llm_http_handler.py New mock-transport tests verify async GET, sync GET, and async list-input-items surface NotFoundError on 404; sync list_responses_input_items path is untested.
tests/test_litellm/litellm_core_utils/test_exception_mapping_utils.py Adds a focused unit test pinning Azure 404 + invalid_request_error body to NotFoundError with correct status code; straightforward and correct.

Reviews (2): Last reviewed commit: "fix(responses): surface upstream error s..." | Re-trigger Greptile

Comment thread litellm/llms/custom_httpx/llm_http_handler.py
Comment thread tests/test_litellm/llms/custom_httpx/test_llm_http_handler.py
@codecov

codecov Bot commented Jul 6, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@mateo-berri
mateo-berri force-pushed the litellm_fix_responses_get_error_body branch from 1aa2a1d to ab79f82 Compare July 6, 2026 22:54
@mateo-berri

Copy link
Copy Markdown
Contributor Author

@greptileai

@mateo-berri
mateo-berri force-pushed the litellm_fix_responses_get_error_body branch from ab79f82 to 4e0a514 Compare July 7, 2026 04:45
@mateo-berri
mateo-berri merged commit 7d6a080 into litellm_internal_staging Jul 7, 2026
124 checks passed
@mateo-berri
mateo-berri deleted the litellm_fix_responses_get_error_body branch July 7, 2026 05:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants