Add structured error handling for Horizon and anchor failures - #257
Conversation
Introduce ProviderError (src/lib/provider-error.ts), a typed AppError carrying a normalized failure category (timeout, transport, rate_limited, unavailable, malformed, rejected) plus safe provider/operation identifiers. All Horizon and SEP-24 anchor I/O under src/services/* now converts provider failures through toProviderError so Fastify handlers and the worker see categories instead of raw SDK exceptions. - Routes: dependency failures answer 502 UPSTREAM_ERROR; provider rejections answer 502 PROVIDER_REJECTED; escaping timeout/transport errors get a safe 502 instead of a generic 500. Successful response shapes are unchanged. - Safety: client messages and logs never carry tokens, authorization headers, signed XDRs, or raw upstream payloads — only validated identifier tokens (e.g. Horizon result codes). stellar.ts now logs via Pino with safe fields only. - Workers: classifyJobFailure reads the typed category first (timeout → indeterminate ledger check, rate-limit/outage → transient, rejection → permanent); anchor poll results persist their category for retry decisions. - Untyped failures handled without throwing: timeouts, non-success HTTP statuses, non-JSON bodies, and schema-mismatched responses are all categorized. Closes mergepay#179
|
@kingAkinwale Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
|
MergeKeeper review Scope: in scope for linked issue The pull request correctly implements structured error handling for Stellar Horizon and SEP-24 anchor failures in accordance with the issue requirements. Reviewed commit: |
|
MergeKeeper merge status Status: blocked Reason: One or more required CI checks failed. Failing checks:
Next steps:
|
Closes #179
Summary
Gives callers and operators consistent, safe error behavior when Stellar Horizon or SEP-24 anchor requests fail. Provider failures are converted to typed, categorized errors inside
src/services/*, mapped to the repository's standard{ code, message, requestId }envelope by the existing central error handler, and classified for worker retry decisions — without exposing tokens, authorization headers, signed XDRs, or raw upstream payloads.Changes
New:
ProviderError(src/lib/provider-error.ts)AppErrorwith a normalized category:timeout|transport|rate_limited|unavailable|malformed|rejectedhorizon,Anchor.getChallenge) for debuggingtx_bad_seq) may be appended to messages/details; everything else from an upstream error is droppedPROVIDER_REJECTEDerror code (502) so a permanent transaction rejection is machine-distinguishable from a transient dependency failure (UPSTREAM_ERROR)Normalization helper (
src/services/timeout.ts)toProviderError()maps anything a provider call threw: TimeoutError →timeout, TransportError →transport, Horizon result codes →rejected, upstream 429 →rate_limited, 5xx →unavailable, other 4xx →rejected, unknown →unavailableServices (all I/O stays inside
src/services/*, mockable)anchor.ts: SEP-10/SEP-24 fetch paths throw categorizedProviderErrors; unguardedres.json()on non-JSON bodies (previously an untyped 500) is nowmalformed;pollTransactionresults carry their failurecategoryfor the workerstellar.ts: loadAccount/submit/lookup failures normalized; replacedconsole.errorwith Pino logging only safe fields (operation,category,resultCodes)network.ts: fee-stats failures normalized the same wayError handler (
src/plugins/error-handler.ts)Worker retry decisions (
src/services/job-retry.ts,src/worker/index.ts)classifyJobFailureconsults the typed category first: timeout → indeterminate (ledger check before resubmit), rate-limit/outage → transient, rejection → permanent — no message-text guessingerrorCategoryStatus codes
UPSTREAM_ERRORPROVIDER_REJECTEDINTERNAL_ERRORSuccessful response shapes are unchanged.
Testing
tests/provider-errors.test.ts(21 tests): mocked-fetch coverage of timeout, non-success HTTP status, non-JSON and schema-mismatched responses, provider rejection with result codes, successful shapes, worker classification, and route-level envelopes viaapp.inject(simulated provider outage)npm run build✅ ·npm test✅ (757 passed) ·npm run lint✅ (0 errors)