Skip to content

fix: classify request timeouts distinctly instead of unknown error - #239

Open
reeshika-h wants to merge 2 commits into
developmentfrom
fix/DX-9991-timeout-error-handling
Open

fix: classify request timeouts distinctly instead of unknown error#239
reeshika-h wants to merge 2 commits into
developmentfrom
fix/DX-9991-timeout-error-handling

Conversation

@reeshika-h

@reeshika-h reeshika-h commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Problem

Under retryResponseErrorHandler, an axios client-side timeout (error.code === 'ECONNABORTED') was converted into a plain object literal — { error_message, error_code, errors } — instead of a real Error. That object has no .message, .request, or .response.

APIError.fromAxiosError only classifies errors by checking err.response?.data, then err.request != null && response == null, then err.message. The plain object matches none of those, so it fell through to the final catch-all and surfaced to SDK consumers as:

error_code: 'UNKNOWN_ERROR', status: 0, error_message: 'An unknown error occurred...'

This is indistinguishable from a genuine network-layer failure (DNS resolution failure, connection reset, etc.) — a legitimate 30s request timeout gave consumers zero information that it was, in fact, a timeout.

Confirmed via git blame this was an unintended regression from a well-intentioned change (commit feb5bfb, Jun 2024) that tried to give timeouts a distinct 408/TIMEOUT identity, but never updated fromAxiosError to recognize the shape it produced.

Fix

Throw a real Error with .message set to the existing timeout message and .code set to the TIMEOUT constant, instead of a plain object literal. No changes needed in api-error.ts — its existing err.message branch already produces a correct, distinct APIError once the thrown value is a proper Error.

Also removed the /* eslint-disable @typescript-eslint/no-throw-literal */ at the top of the file — nothing throws a literal anymore.

Scope

This fix only corrects error classification. It does not change whether a timed-out request is retried (unchanged: ECONNABORTED is not retried unless a custom retryCondition matches it) and does not touch connection pooling/keepAlive defaults. Those are tracked separately as follow-up SDK-side improvements under the same ticket, to be reviewed and shipped independently given they change default behavior for all consumers.

Tests

  • Added an integration-style test that runs a real ECONNABORTED error through the actual retryResponseErrorHandlerAPIError.fromAxiosError pipeline (no mocking either side) — reproduces the reported bug before the fix (error_code: 'UNKNOWN_ERROR' instead of 408).
  • Updated the one existing unit test that asserted the old, broken plain-object shape.
  • Full suite green (145/146; the one unrelated failure is a pre-existing stale-dist-build check, confirmed via git stash to predate this change).
  • 100% statement/line/function coverage, ~97% branch coverage on both touched files.

Test plan

  • Unit tests pass (npm test)
  • Lint passes with no new warnings/errors
  • Reviewer confirms the classification change doesn't surprise any downstream consumer relying on the old plain-object shape (none found in this repo's own call sites)

retryResponseErrorHandler threw a plain object literal (error_message/error_code
fields, no .message) for axios ECONNABORTED (client-side timeout) errors.
APIError.fromAxiosError only recognizes .response.data, .request, or .message,
so the plain object matched none of those and collapsed to a generic
UNKNOWN_ERROR/status:0 - indistinguishable from a true network-layer failure,
with no indication the request had actually timed out.

Throw a real Error with .message and .code set instead, so it flows through
the existing err.message branch in fromAxiosError and surfaces as a distinct,
diagnosable TIMEOUT (408) error.

DX-9991
No longer needed now that the ECONNABORTED branch throws a real Error
instead of a plain object literal.

DX-9991
@reeshika-h
reeshika-h requested a review from a team as a code owner July 28, 2026 07:45
@github-actions

Copy link
Copy Markdown

🔒 Security Scan Results

ℹ️ Note: Only vulnerabilities with available fixes (upgrades or patches) are counted toward thresholds.

Check Type Count (with fixes) Without fixes Threshold Result
🔴 Critical Severity 0 0 10 ✅ Passed
🟠 High Severity 0 0 25 ✅ Passed
🟡 Medium Severity 0 0 500 ✅ Passed
🔵 Low Severity 0 0 1000 ✅ Passed

⏱️ SLA Breach Summary

✅ No SLA breaches detected. All vulnerabilities are within acceptable time thresholds.

Severity Breaches (with fixes) Breaches (no fixes) SLA Threshold (with/no fixes) Status
🔴 Critical 0 0 15 / 30 days ✅ Passed
🟠 High 0 0 30 / 120 days ✅ Passed
🟡 Medium 0 0 90 / 365 days ✅ Passed
🔵 Low 0 0 180 / 365 days ✅ Passed

✅ BUILD PASSED - All security checks passed

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