Skip to content

fix(sdk): handle non-JSON error responses, add HTTP retry, defensive null checks#1734

Open
DukeDeSouth wants to merge 1 commit intoupstash:masterfrom
DukeDeSouth:fix/sdk-http-client-robustness
Open

fix(sdk): handle non-JSON error responses, add HTTP retry, defensive null checks#1734
DukeDeSouth wants to merge 1 commit intoupstash:masterfrom
DukeDeSouth:fix/sdk-http-client-robustness

Conversation

@DukeDeSouth
Copy link

@DukeDeSouth DukeDeSouth commented Feb 7, 2026

Human View

Summary

Fixes three related robustness issues in the @upstash/context7 SDK's HTTP client and command layer:

Bug 1: SyntaxError crash on non-JSON error responses

Before: When the Context7 API (or an intermediary proxy) returned a non-JSON error response (e.g., HTML 502 Bad Gateway page, plain text), HttpClient.request() called res.json() which threw an uncaught SyntaxError. Users saw a confusing SyntaxError: Unexpected token < in JSON at position 0 instead of a meaningful Context7Error.

After: The error body is read once as text via res.text(), then JSON.parse is attempted. If parsing fails, short text bodies (<=200 chars) are used directly as the error message; long bodies (HTML pages) fall back to statusText (status). The error is always wrapped in a Context7Error.

Bug 2: No retry on HTTP 429/5xx status codes

Before: The retry loop only caught network-level fetch failures (DNS errors, connection refused, etc.). Server errors (500, 502, 503, 504) and rate limits (429) were not retried, even though the client was configured with exponential backoff and up to 5 retries. This is especially impactful because Context7's own MCP server returns 429 for rate-limited requests.

After: HTTP 429, 500, 502, 503, and 504 responses are now retried with the configured backoff strategy. The Retry-After header is respected on 429 responses. Non-retryable client errors (400, 401, 404) are still immediately thrown.

Bug 3: Crash on incomplete API response fields

Before: SearchLibraryCommand.exec() assumed result.results was always a valid array, and GetContextCommand.exec() assumed apiResult.codeSnippets and apiResult.infoSnippets were always present. Missing or null fields caused TypeError: Cannot read properties of undefined (reading 'map').

After: Added nullish coalescing for array fields and an Array.isArray guard for search results. Missing fields now gracefully return empty arrays.

Additional fix: retry: false created 2 attempts instead of 1

The constructor set attempts: 1 for retry: false, but the loop condition i <= attempts produced 2 iterations. Fixed to attempts: 0 (1 total attempt, 0 retries).

Test plan

Added 39 new unit tests (mocked fetch, no API key needed):

  • JSON error body parsing (error field, message field, statusText fallback)
  • HTML error body from proxy (no SyntaxError crash)
  • Long non-JSON body truncation
  • Short plain text error body
  • Empty error body fallback to statusText
  • Network failure retry + eventual success
  • Persistent network failure exhausts retries
  • Abort signal prevents retry
  • HTTP 429 retry + success
  • HTTP 500/502/503/504 retry + success
  • HTTP 400/401/404 NOT retried (client errors)
  • Persistent 500 exhausts all retries
  • Retry-After header respected on 429
  • Mixed failures (network error -> 502 -> success)
  • retry: false prevents any retry
  • Missing results field in search response
  • Missing codeSnippets/infoSnippets in context response
  • Null fields, non-array fields, undefined results
  • Correct formatting of valid responses

All 39 tests pass locally.


AI View (DCCE Protocol v1.0)

Metadata

  • Generator: Claude (Anthropic) via Cursor IDE
  • Methodology: AI-assisted development with human oversight and review

AI Contribution Summary

  • Solution design and implementation
  • Test development (39 new test cases)

Verification Steps Performed

  1. Reproduced the reported issue
  2. Analyzed source code to identify root cause
  3. Implemented and tested the fix
  4. Ran full test suite (39 tests passing)
  5. Verified lint/formatting compliance

Human Review Guidance

  • Core changes are in: JSON.parse, result.results

Made with M7 Cursor

…null checks

**Problem:**
1. `HttpClient.request()` crashes with `SyntaxError` when the server returns
   non-JSON error responses (HTML from proxies, plain text). The code called
   `res.json()` without catching parse failures, so users got a confusing
   `SyntaxError` instead of a meaningful `Context7Error`.

2. Only network-level `fetch` failures were retried. HTTP 429 (rate limit)
   and 5xx (server errors) were not retried, even though the client is
   configured with exponential backoff and up to 5 retries.

3. `SearchLibraryCommand` and `GetContextCommand` assumed API response
   fields (`results`, `codeSnippets`, `infoSnippets`) were always present.
   If the API returned an incomplete response, the commands crashed with
   `Cannot read properties of undefined (reading 'map')`.

4. `retry: false` incorrectly set `attempts: 1` (2 total tries) instead
   of `attempts: 0` (1 total try, no retries).

**Fix:**
1. Read error body once as text, then attempt `JSON.parse`. Long non-JSON
   bodies (>200 chars) fall back to `statusText (status)`. Short text
   bodies are used directly as the error message.

2. Added retry logic for HTTP 429, 500, 502, 503, 504. Respects
   `Retry-After` header on 429 responses. Uses the configured exponential
   backoff function as fallback.

3. Added nullish coalescing (`?? []`) for array fields from API responses,
   and an explicit `Array.isArray` guard for search results.

4. Fixed `retry: false` to set `attempts: 0`.

**Tests:** 39 new unit tests covering all error handling paths, retry
scenarios (network + HTTP status), defensive parsing, and edge cases.

Co-authored-by: Cursor <cursoragent@cursor.com>
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.

1 participant

Comments