Skip to content

fix: preserve HTTP status for API errors - #238

Merged
drish merged 2 commits into
resend:mainfrom
mayankbohradev:fix/preserve-http-status-non-json-errors
Jul 27, 2026
Merged

fix: preserve HTTP status for API errors#238
drish merged 2 commits into
resend:mainfrom
mayankbohradev:fix/preserve-http-status-non-json-errors

Conversation

@mayankbohradev

@mayankbohradev mayankbohradev commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

What

Preserve the real HTTP status code when the Resend API returns an error.

This now works for both kinds of error responses:

  • non-JSON responses, such as HTML from a proxy or an empty gateway response
  • valid JSON responses that do not include a statusCode field

The change applies to both sync and async requests.

The bug

The HTTP clients already returned the response body, HTTP status, and headers. The request layer kept the body and headers but discarded the HTTP status.

It then decided whether a request failed by looking only for statusCode inside the JSON body. If the API returned HTTP 429 with this valid JSON:

{
  "name": "rate_limit_exceeded",
  "message": "Too many requests"
}

the SDK found no JSON statusCode and returned the error object as normal successful data.

Non-JSON error responses had a related problem: their real 401, 429, 502, or 503 status was replaced with 500.

Fix

Each request now stores the HTTP status together with the response headers.

When deciding whether to raise an exception:

  1. a real HTTP 4xx or 5xx status takes priority
  2. JSON statusCode remains supported for backward compatibility
  3. if neither indicates an error, the response is returned normally

This keeps the existing typed exception mapping. For example, HTTP 429 with name: rate_limit_exceeded now raises RateLimitError with code == 429.

Test proof

New regression tests cover the exact false-success path:

  • sync HTTP 429 + valid JSON without statusCode raises RateLimitError
  • async HTTP 422 + valid JSON without statusCode raises ValidationError
  • the original error message and response headers are preserved

The PR also keeps its existing defensive coverage:

  • HTTP 429 + HTML preserves 429
  • HTTP 503 + plain text preserves 503
  • HTTP 502 + invalid JSON preserves 502
  • HTTP 200 + unusable HTML falls back to 500
  • async HTTP 401 + HTML preserves 401

Local validation after rebasing onto current main:

  • tox -e lint: passed
  • tox -e mypy: passed, 167 source files
  • tox -e py -- tests/request_test.py: 9 passed
  • tox -e py: 500 passed
  • Python syntax and changed-file secret scan: passed

Compatibility

Successful responses are unchanged. JSON error bodies that already include statusCode continue to work as before.

@codecov-commenter

codecov-commenter commented Jul 11, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 96.42%. Comparing base (7f1acd0) to head (98943ad).
⚠️ Report is 194 commits behind head on main.

Additional details and impacted files
@@             Coverage Diff             @@
##             main     #238       +/-   ##
===========================================
+ Coverage   82.66%   96.42%   +13.75%     
===========================================
  Files           4       65       +61     
  Lines          75     3525     +3450     
===========================================
+ Hits           62     3399     +3337     
- Misses         13      126      +113     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@github-actions github-actions Bot added the linear-synced PR has been synced to Linear label Jul 13, 2026
When the response body is not usable JSON (CDN HTML, empty 5xx,
proxies), use the real HTTP status for 4xx/5xx instead of always
raising a fake 500. Mirror sync and async request paths.
@mayankbohradev
mayankbohradev force-pushed the fix/preserve-http-status-non-json-errors branch from 8451c2d to 98943ad Compare July 27, 2026 09:30
@mayankbohradev mayankbohradev changed the title fix: preserve HTTP status for non-JSON API errors fix: preserve HTTP status for API errors Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

Updated this PR after rebasing it onto the current main branch.

The original version preserved the HTTP status for non-JSON responses. The request layer had the same false-success bug for valid JSON error responses that omitted statusCode.

Example before this update:

  • HTTP status: 429
  • Body: {"name":"rate_limit_exceeded","message":"Too many requests"}
  • SDK result: returned as normal data

After this update, the same response raises RateLimitError with code == 429, keeps the original message, and preserves the retry-after header.

New regression proof:

  • Sync HTTP 429 JSON without statusCode raises RateLimitError
  • Async HTTP 422 JSON without statusCode raises ValidationError
  • Focused request tests: 9 passed
  • Full test suite: 500 passed
  • Lint: passed
  • Mypy: passed across 167 source files

Successful responses and JSON errors that already include statusCode remain unchanged.

@dielduarte
dielduarte requested a review from drish July 27, 2026 15:08
@drish
drish merged commit 04aab58 into resend:main Jul 27, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

linear-synced PR has been synced to Linear

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants