feat(http): map 4xx/5xx responses to typed SDK errors (closes #2) - #7
Merged
David-patrick-chuks-02 merged 2 commits intoSep 3, 2026
Conversation
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>
Contributor
|
Addressed in #166. Error mapping — already implemented, added tests — implementation + tests included. Bounty: $0 |
1 similar comment
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
merged commit Sep 3, 2026
bb07910
into
Lilly-Protocol:main
0 of 7 checks passed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #2.
Every non-ok response except 401/403 became a generic
LilyApiError, so callers had to readstatusCodeor parse the body to tell "not found" from "rate limited".Mapping
codeLilyValidationErrorVALIDATION_ERRORLilyAuthenticationErrorAUTHENTICATION_ERRORLilyAuthorizationErrorAUTHORIZATION_ERRORLilyNotFoundErrorNOT_FOUNDLilyConflictErrorCONFLICTLilyRateLimitError(+retryAfterSeconds)RATE_LIMITEDLilyServerErrorSERVER_ERRORLilyApiErrorAPI_ERRORThe new classes extend
LilyApiError(andLilyAuthorizationErrorextendsLilyAuthenticationError), so existinginstanceofchecks keep working — there are tests asserting exactly that, since it's the thing a mapping change most easily breaks.Retry-Afteris only reported in its delta-seconds form. The HTTP-date form would mean guessing at clock skew, so it's leftundefinedrather than wrong.Not leaking secrets
Errors now carry a redacted
bodySnippetnext to the fulldetails. Values under keys matchingpass|secret|token|api[-_]?key|authorization|credential|signature|privateare 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 makingJSON.stringifythrow and losing the snippet entirely.One bug this surfaced
The
catchblock rethrew onlyLilyApiErrorandLilyAuthenticationError.LilyValidationErrorextendsLilySdkErrordirectly, so a mapped 400 was being caught and re-wrapped as aLilyTransportError, losingstatusCodeanddetails. 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 realcreateFetchHttpClientwith a mocked fetch rather than calling the mapper directly:LilySdkError; all non-auth ones remainLilyApiErrorRetry-Afterin both seconds and HTTP-date formdetailsstill carries the full parsed bodyGate
Windows / Node 20:
npx vitest run— 29 passed (7 before this branch)npx tsc --project tsconfig.json --noEmit— clean (repo hasexactOptionalPropertyTypes, so the new optional fields are declared?: T | undefined)npx eslint .— cleannpm run format:checkisn't usable as a signal here:prettier --checkreports 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.gitattributesin a separate one if that's useful.🤖 Generated with Claude Code