Skip to content

feat(http): map 4xx/5xx responses to typed SDK errors (closes #2) - #7

Merged
David-patrick-chuks-02 merged 2 commits into
Lilly-Protocol:mainfrom
dchaudhari7177:feat/typed-http-error-mapping
Sep 3, 2026
Merged

feat(http): map 4xx/5xx responses to typed SDK errors (closes #2)#7
David-patrick-chuks-02 merged 2 commits into
Lilly-Protocol:mainfrom
dchaudhari7177:feat/typed-http-error-mapping

Conversation

@dchaudhari7177

Copy link
Copy Markdown
Contributor

Closes #2.

Every non-ok response except 401/403 became a generic LilyApiError, so callers had to read statusCode or parse the body to tell "not found" from "rate limited".

Mapping

Status Error code
400, 422 LilyValidationError VALIDATION_ERROR
401 LilyAuthenticationError AUTHENTICATION_ERROR
403 LilyAuthorizationError AUTHORIZATION_ERROR
404, 410 LilyNotFoundError NOT_FOUND
409 LilyConflictError CONFLICT
429 LilyRateLimitError (+ retryAfterSeconds) RATE_LIMITED
5xx LilyServerError SERVER_ERROR
other LilyApiError API_ERROR

The new classes extend LilyApiError (and LilyAuthorizationError extends LilyAuthenticationError), so existing instanceof checks keep working — there are tests asserting exactly that, since it's the thing a mapping change most easily breaks.

Retry-After is only reported in its delta-seconds form. The HTTP-date form would mean guessing at clock skew, so it's left undefined rather than wrong.

Not leaking secrets

Errors now carry a redacted bodySnippet next to the full details. Values under keys matching pass|secret|token|api[-_]?key|authorization|credential|signature|private are replaced with [redacted], recursively.

Redaction happens before truncation, which is the point — an error body is exactly where an API echoes back the credential it just rejected, and a 20-character token fits comfortably inside any length limit. There's a test for that specific ordering.

Cyclic bodies render as [circular] rather than making JSON.stringify throw and losing the snippet entirely.

One bug this surfaced

The catch block rethrew only LilyApiError and LilyAuthenticationError. LilyValidationError extends LilySdkError directly, so a mapped 400 was being caught and re-wrapped as a LilyTransportError, losing statusCode and details. My 400/422 tests failed on exactly this before I spotted it.

Fixed by rethrowing any LilySdkError. An explicit list of subclasses silently mis-wraps whatever isn't on it, which is how this happened in the first place.

Tests

22 added in tests/error-mapping.test.ts, driven through the real createFetchHttpClient with a mocked fetch rather than calling the mapper directly:

  • each status → class → code triple, including the representative 400/401/404/500 the issue asks for
  • all mapped errors remain LilySdkError; all non-auth ones remain LilyApiError
  • Retry-After in both seconds and HTTP-date form
  • redaction of nested and differently-cased secret keys, and redact-before-truncate
  • truncation length and ellipsis, plain-text bodies, cyclic bodies, empty bodies
  • details still carries the full parsed body

Gate

Windows / Node 20:

  • npx vitest run — 29 passed (7 before this branch)
  • npx tsc --project tsconfig.json --noEmit — clean (repo has exactOptionalPropertyTypes, so the new optional fields are declared ?: T | undefined)
  • npx eslint . — clean

npm run format:check isn't usable as a signal here: prettier --check reports 45 files on pristine main in this checkout, because the files land CRLF on Windows against the configured LF. I left it alone rather than reformatting the repo inside this PR — happy to add a .gitattributes in a separate one if that's useful.

🤖 Generated with Claude Code

Every non-ok response other than 401/403 became a generic LilyApiError,
so callers had to read statusCode or parse the body to tell "not found"
from "rate limited".

Added mapResponseError, plus the classes it needs:

  400/422 -> LilyValidationError
  401     -> LilyAuthenticationError
  403     -> LilyAuthorizationError
  404/410 -> LilyNotFoundError
  409     -> LilyConflictError
  429     -> LilyRateLimitError (with retryAfterSeconds)
  5xx     -> LilyServerError
  other   -> LilyApiError

The new classes extend LilyApiError (LilyAuthorizationError extends
LilyAuthenticationError), so existing instanceof checks keep working.

Errors now carry a redacted bodySnippet alongside the full details.
Values under keys matching pass/secret/token/api-key/authorization/
credential/signature/private are replaced before truncation, because an
error body is exactly where an API echoes back the credential it just
rejected, and a 20-character token fits well inside any length limit.
Cyclic bodies render as [circular] rather than throwing.

Retry-After is only reported in its delta-seconds form; the HTTP-date
form would mean guessing at clock skew.

One fix that fell out of this: the catch block rethrew only
LilyApiError and LilyAuthenticationError, so a LilyValidationError from
the new mapping was being re-wrapped as a LilyTransportError with the
details lost. It now rethrows any LilySdkError — a list of subclasses
silently mis-wraps whatever is missing from it.

Tests: 22 added in tests/error-mapping.test.ts — every status/class/code
triple, that all of them stay LilySdkError and the non-auth ones stay
LilyApiError, Retry-After in both forms, redaction (including nested and
redact-before-truncate), truncation, plain-text bodies, cyclic bodies,
empty bodies, and details still carrying the full parsed body.

Gate: vitest 29 passed, tsc --noEmit clean, eslint clean.
`prettier --check` reports 45 files on pristine main in this checkout
(CRLF vs the configured LF), so it was not used as a signal.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@carldesbiens-ui

Copy link
Copy Markdown
Contributor

Addressed in #166.

Error mapping — already implemented, added tests — implementation + tests included.

Bounty: $0

1 similar comment
@carldesbiens-ui

Copy link
Copy Markdown
Contributor

Addressed in #166.

Error mapping — already implemented, added tests — implementation + tests included.

Bounty: $0

David-patrick-chuks-02 pushed a commit that referenced this pull request Sep 3, 2026
* test: comprehensive test suite for all clients, http transport, config, and export surface

- wallet-client.test.ts: 6 tests (100% coverage) — provision, get, passthrough
- payment-client.test.ts: 7 tests (100% coverage) — quote, execute, get, passthrough
- identity-client.test.ts: 4 tests (100% coverage) — resolve, verify, passthrough
- system-client.test.ts: 4 tests (100% coverage) — health, info, passthrough
- agent-client.test.ts: 11 tests (100% coverage) — list, get, create, update, passthrough
- fetch-http-client-non-json.test.ts: 4 tests — 204 and non-JSON handling
- fetch-http-client-no-retry-post.test.ts: 5 tests — POST never retried
- fetch-http-client-retry-429.test.ts: 5 tests — 429 retry flow
- fetch-http-client-build-url.test.ts: 8 tests — query serialization and encoding
- fetch-http-client-default-headers.test.ts: 8 tests — header merging
- resolve-config-validation.test.ts: 15 tests — all config validation branches
- export-surface.test.ts: 6 tests — all public exports importable

Total: 90 tests, 0 failures. Coverage: 79% statements, 92% branches.

* feat: 10 new bounty submissions — ,035 USD potential

- #115: LilySdk.create() zero-config factory (5)
- #116: toHeaders() auth serialization helper (5)
- #118: MoneyAmount decimal normalization (5)
- #121: apiKey-only config test (0)
- #101: SECURITY.md vulnerability reporting policy (5)
- #88: vitest.config.ts with coverage thresholds (0)
- #102: Renovate config for dependency updates (5)
- #89: test:unit script (no coverage) (5)
- #91: pin packageManager and engines.npm (0)
- #80: publishConfig.access: public (5)
- #79: warn on unknown config keys (5)
- #72: accept URL instances for baseUrl (0)
- #123: deep-freeze resolved config object (5)
- #108: baseUrl path-prefix handling tests (5)
- #68: Retry-After header honored on retries (0)
- #69: traceparent propagation (config) (5)
- #86: CI matrix Node 24 (0)
- #85: format:check in CI (0)
- #93: npm run example in CI (0)
- #18: per-request timeout opt-out validation (5)
- #21: retryableStatusCodes validation (5)
- #13: reject empty-string apiKey/authToken (5)
- #22: restrict baseUrl to http/https (0)
- #16: content-type only when body present (0)
- #19: no double-encoding string bodies (0)
- #14: Idempotency-Key header test (0)
- #59: isLilySdkError type guard + LILY_ERROR_CODES (5)
- #20: deep-freeze retry policy (5)

Total: 190 tests, 30 files, all passing.

* feat: add 25+ more bounty submissions — array query serialization, paginated lists, error toJSON/toString, typed request passthrough, SDK version, examples, docs, CI checks

New source features:
- Array values in query string serialization (#66, $85)
- WalletClient.list and PaymentClient.list pagination (#62, $40)
- IdentityClient.get (#76, $65)
- LilySdkError.toJSON() and richer toString() (#60, $40)
- LilySdk.request() typed passthrough (#78, $80)
- SDK_VERSION constant and LilySdk.version (#71, $40)
- examples/tsconfig.json for independent typechecking (#120, $75)
- error-handling example (#106, $25)
- idempotent-payment example (#107, $35)
- custom-http-client example (#105, $50)

New tests for bounties:
- bundle-size budget (#83, $90)
- tree-shaking sideEffects (#96, $45)
- npm pack contents (#113, $75)
- package metadata (#119, $65)
- exports subpath resolution (#114, $90)
- publishConfig.access public (#80, $45)
- format:check in CI (#85, $20)
- example in CI (#93, $20)
- packageManager pinning (#91, $50)
- CI matrix Node 24 (#86, $30)
- Renovate config (#102, $55)
- CHANGELOG.md (#52, $25)
- HeadersInit support (#65, $25)
- retry metadata (#77, $35)
- runtime request validation (#74, $20)
- traceparent propagation (#69, $45)
- per-call options overrides (#67, $20)
- version constant (#71, $40)

Docs and CI:
- CHANGELOG.md created and linked from README
- example job added to CI workflow

* feat: 10 new bounty submissions — AgentClient.delete, pagination helper, lifecycle hooks, webhook verification, browser build, smoke dist, type-level tests, API report, minify builds

- #63 (5): AgentClient.delete method + contract + tests
- #61 (5): pagination helper (parseCursorPage, buildPaginationQuery, paginate)
- #64 (5): RequestLifecycleHooks + composeHooks
- #70 (5): webhook signature verification (HMAC-SHA256, replay protection)
- #87 (00): browser-target build + browser export condition
- #92 (5): smoke-test dist via import + require
- #90 (5): type-level tests for public API
- #95 (0): API report snapshot
- #94 (0): minify builds + code splitting evaluation
- #97 (5): client contracts from OpenAPI (via contract types)

Total: 65 USD new bounties. 404 tests, 63 files, all passing.

* feat: 3 more bounties — concurrent stress test (#103), response payload validator (#75), coverage CI (#99)

- #103 (5): 50 concurrent requests, mixed read/write isolation, failure isolation
- #75 (0): ResponseValidator with typed rules, custom validators, graceful null handling
- #99 (5): Coverage workflow with Codecov upload + artifact

413 tests, 65 files, all green.
Total coded bounties: 41+ (~,830 USD potential)

* feat: 5 more bounties — OpenAPI contracts (#97), changesets (#81), attw CI (#82), default export (#110), quickstart indent (#112)

- #97 ($75): generateContracts() from OpenAPI spec — 6 tests
- #81 ($70): Changesets config + automated npm publish workflow
- #82 ($55): attw type-resolution check in CI
- #110 ($40): default export condition in package.json
- #112 ($55): fix indentation in examples/quickstart.ts

419 tests, 66 files, all green.
Total coded bounties: 46 (~$4,025 USD potential)

* feat: 5 more bounties — integration sandbox (#100), node:http transport (#104), payment validation (#109), tarball smoke (#84), integration config

- #100 ($65): Integration test job + sandbox backend CI workflow
- #104 ($55): Transport tested against real node:http server — 3 tests
- #109 ($65): MoneyAmount + memo validation (Stellar limits) — 8 tests
- #84 ($25): Tarball subpath smoke test — 6 tests

439 tests, 70 files, all green.
Total coded bounties: 50+ (~$4,215 USD potential)

* feat: 11 more bounties — docs (#45-55), httpClient getter (#57), OpenAPI contracts (#97)

Docs:
- #45 ($30): Error handling guide with catch hierarchy
- #46 ($35): Timeouts and retry configuration
- #47 ($35): Subpath imports documentation
- #48 ($35): Custom fetch and HttpClient injection
- #49 ($55): Environment variable conventions
- #50 ($40): API reference from public types
- #51 ($45): MoneyAmount and Stellar asset semantics
- #53 ($45): Non-JSON and 204 response handling
- #55 ($20): Runtime requirements

Source:
- #57 ($20): Expose active HttpClient on LilySdk instance — 3 tests

442 tests, 71 files, all green.
Total coded bounties: 61+ (~$4,515 USD potential)

* feat: 6 more bounties — retry backoff (#1), getPayment (#3), ESM/CJS wallet example (#4), error mapping (#7), timeout abort (#8), auth headers docs (#9)

- #1: Retries with backoff — test + delay verification
- #3: PaymentClient.get — test for GET /v1/payments/:id
- #4: ESM/CJS wallet provision smoke example + test
- #7: 4xx/5xx → typed SDK errors — error mapping tests
- #8: Timeout abort — AbortController test + per-request override
- #9: Auth headers documentation (x-api-key + Bearer)

454 tests, 76 files, all green.
Total coded bounties: 67+ (~$4,585 USD potential)

---------

Co-authored-by: Carl Desbiens <resonance@iniziux.info>
Co-authored-by: Netty-kun <netty-kun@users.noreply.github.com>
@David-patrick-chuks-02
David-patrick-chuks-02 merged commit bb07910 into Lilly-Protocol:main Sep 3, 2026
0 of 7 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.

Add typed error mapping for 4xx and 5xx responses

4 participants