Bound retries around reads. Different rules for webhook rehearsal.
The TypeScript client retries only idempotent GET reads under a small, configurable budget. The local webhook sandbox has a separate three-attempt policy. Do not apply one policy to the other.
Default Headless client behavior is one initial attempt plus two retries, with an 8-second timeout per attempt and a 5-second maximum delay.
HTTP client policy
| Condition | Automatic retry? | Delay | Final error |
|---|---|---|---|
| 429 with retryable: true | Yes | Retry-After when valid, otherwise exponential | RateLimited when attempts are exhausted |
| 500–599 with retryable: true | Yes | Retry-After when valid, otherwise exponential | Typed envelope error or status fallback |
| 429 or 500–599 with no valid error envelope | Yes | Retry-After when valid, otherwise exponential | Status-mapped CassetApiError |
| 429 or 500–599 with retryable: false | No | None | Typed error immediately |
| Network or per-attempt timeout failure | Yes | Exponential | IntegrationFailure with retryable: true |
| External AbortSignal already aborted, or aborted during transport | No further retry | None | IntegrationFailure with retryable: false |
| 408 Request Timeout response | No | None | Usually IntegrationFailure; status remains 408 |
| 425 Too Early response | No | None | Usually IntegrationFailure; status remains 425 |
| 400, 401, 402, 403, 404, or 422 | No | None | Mapped typed error |
| Malformed 2xx or request-ID mismatch | No | None | IntegrationFailure with retryable: false |
Attempts and delays
| Option | Default | Range | Meaning |
|---|---|---|---|
maxRetries | 2 | 0–5 | Retries after the first attempt. Default: at most 3 total attempts. |
timeoutMs | 8,000 ms | 1–60,000 ms | Per-attempt timeout, not a total request deadline. |
maxRetryAfterMs | 5,000 ms | 0–30,000 ms | Caps both Retry-After and exponential delays. |
Without Retry-After, delay ismin(100 × 2^attemptIndex, maxRetryAfterMs). With defaults, the client waits 100 ms before attempt two and 200 ms before attempt three. It adds no jitter.
Retry-After
- A non-negative finite numeric value accepted by JavaScript
Numberis interpreted as seconds, converted to milliseconds, rounded, and capped. - An HTTP date is converted to a non-negative delay and capped.
- An invalid value is ignored and exponential delay is used.
- The parsed bounded value is available as
error.retryAfterMsafter failure.
Configure the policy
const casset = createCassetClient({
baseUrl: "http://127.0.0.1:3000",
timeoutMs: 5_000,
maxRetries: 1, // two total attempts
maxRetryAfterMs: 2_000,
})Set maxRetries: 0 to disable automatic retries. SetmaxRetryAfterMs: 0 to remove retry waits without removing attempts. There is no separate jitter switch because the implementation adds no jitter.
Worst-case duration
| Configuration | Upper bound | Composition |
|---|---|---|
| Default HTTP responses | About 34 seconds | Three 8-second attempt budgets plus two delays capped at 5 seconds. |
| Default network or timeout failures | About 24.3 seconds | Three 8-second attempt budgets plus deterministic 100 ms and 200 ms waits. |
| Maximum accepted configuration | About 510 seconds | Six 60-second attempt budgets plus five delays capped at 30 seconds. |
These bounds apply to the built-in fetch transport, which observes the per-attempt AbortSignal, plus normal scheduler overhead. A custom transport that ignores its signal can outlive timeoutMs, so the SDK cannot state a hard wall-clock bound for that transport.
Failures that never retry
- Malformed handles, invalid configuration, and invalid request IDs fail before transport.
- An external signal already aborted at an attempt boundary, 408, 425, 400–404, 422, and any envelope with
retryable: falseare terminal. - The built-in retry sleep is not abort-aware. If cancellation happens during that delay, the client enters the next attempt with an already-aborted signal and then fails terminally; do not treat the signal as an immediate total-deadline timer.
- Malformed success bodies and request-ID mismatches are terminal contract failures.
- The client exposes GET only; there is no SDK write method to retry.
Application-level recovery
The SDK has already spent its retry budget when it throws. Do not immediately wrap every call in another tight loop. For interactive UI, show a retry control after a retryable error. For a background job, add a larger, independently bounded schedule and keep the same request ID only for one SDK call—not across unrelated jobs.
Idempotency expectations
| Surface | Behavior |
|---|---|
| Public v1 reads | GET-only and naturally idempotent. They do not accept an Idempotency-Key. |
| SDK retries | Reuse one x-request-id across every attempt of the same logical GET. |
| Sandbox console mutations | Require Idempotency-Key, bounded to 128 text characters. |
| Same key + same action type and payload | Returns the stored result; concurrent duplicates coalesce. |
| Same key + different payload within one action type | Returns 409 INVALID_REQUEST. |
| Same key text + different action type | Uses a separate action-scoped record and does not conflict. |
| One-time values on replay | Stored credential and webhook results redact plaintext tokens and signing secrets. |
Local sandbox webhook policy
| Outcome | Retry? | Notes |
|---|---|---|
| 2xx | No | Delivery succeeds. |
| 408, 425, 429, or 500+ | Yes | Recorded as HTTP_TRANSIENT. |
| Other non-2xx, including 400–407, 409–424, and 426–499 | No | Recorded as HTTP_TERMINAL. |
| Network failure | Yes | Recorded as NETWORK. |
| No response within 2 seconds | Yes | Recorded as TIMEOUT. |
- Maximum: three total attempts.
- Timeout: two seconds per attempt.
- Sandbox delay after a retryable failure: 25 ms after attempt one, then 50 ms after attempt two.
- The delay is deterministic and deliberately small for tests; it is not the HTTP client's exponential policy.
Replay and duplicate suppression
Replaying a sandbox webhook preserves the immutable event ID and exact event body, then creates a new delivery ID. The receiver validates the exact event contract before deduplication and reports whether the delivery/event pair was already received. Concurrent console replay with the same idempotency key resolves to one delivery result.
Webhook signature window
- The HMAC input is
timestamp + "." + exactBody. - The receiver verifies the exact bytes; adding a newline invalidates the signature.
- Signatures outside a five-minute window are rejected.
- The receiver reads at most 128 KiB of UTF-8 request data.