MCPcopy Create free account
hub / github.com/TestSprite/testsprite-cli / apiRetryDecision

Function apiRetryDecision

src/lib/http.ts:726–779  ·  view source on GitHub ↗
(
  code: ErrorCode,
  attempt: number,
  retryAfterSec: number | undefined,
  random: () => number,
  retryOnConflict = true,
  retryOnRateLimit = true,
)

Source from the content-addressed store, hash-verified

724}
725
726function apiRetryDecision(
727 code: ErrorCode,
728 attempt: number,
729 retryAfterSec: number | undefined,
730 random: () => number,
731 retryOnConflict = true,
732 retryOnRateLimit = true,
733): RetryDecision {
734 switch (code) {
735 // M3.2 piece-1 / piece-2: `PRECONDITION_FAILED` (etag mismatch),
736 // `IDEMPOTENCY_BODY_MISMATCH` (caller mutated body across retries),
737 // and `PAYLOAD_TOO_LARGE` (350 KB cap) are all caller-side bugs —
738 // never retry, exit immediately with the typed envelope.
739 // INSUFFICIENT_CREDITS is non-retriable: out-of-credits cannot self-heal.
740 // The RATE_LIMITED credits sub-case is re-mapped to INSUFFICIENT_CREDITS
741 // in parseEnvelopeBody (errors.ts) before apiRetryDecision is called,
742 // so the genuine per-minute throttle (RATE_LIMITED) reaches the RATE_LIMITED
743 // case below and retries normally.
744 // FEATURE_GATED is non-retriable: a paid-feature gate can't self-heal with
745 // retries — the caller must upgrade their plan first.
746 case 'AUTH_REQUIRED':
747 case 'AUTH_INVALID':
748 case 'AUTH_FORBIDDEN':
749 case 'NOT_FOUND':
750 case 'VALIDATION_ERROR':
751 case 'PAYLOAD_TOO_LARGE':
752 case 'PRECONDITION_FAILED':
753 case 'IDEMPOTENCY_BODY_MISMATCH':
754 case 'UNSUPPORTED':
755 case 'INSUFFICIENT_CREDITS':
756 case 'FEATURE_GATED':
757 return { retry: false, delayMs: 0 };
758 case 'CONFLICT':
759 // Read paths (e.g. GET /failure) retry once: 409 = mid-mutation snapshot.
760 // Write paths (e.g. POST /runs) must NOT retry: 409 = another run is in
761 // flight (persistent), retrying would enqueue a second run.
762 if (!retryOnConflict) return { retry: false, delayMs: 0 };
763 if (attempt >= MAX_ATTEMPTS_CONFLICT) return { retry: false, delayMs: 0 };
764 return { retry: true, delayMs: CONFLICT_DELAY_MS };
765 case 'INTERNAL':
766 if (attempt >= MAX_ATTEMPTS_INTERNAL) return { retry: false, delayMs: 0 };
767 return { retry: true, delayMs: INTERNAL_DELAY_MS };
768 case 'RATE_LIMITED':
769 if (!retryOnRateLimit) return { retry: false, delayMs: 0 };
770 if (attempt >= MAX_ATTEMPTS_RATE_LIMITED) return { retry: false, delayMs: 0 };
771 return {
772 retry: true,
773 delayMs: Math.min(Math.max(0, (retryAfterSec ?? 1) * 1000), MAX_RATE_LIMITED_DELAY_MS),
774 };
775 case 'UNAVAILABLE':
776 if (attempt >= MAX_ATTEMPTS_UNAVAILABLE) return { retry: false, delayMs: 0 };
777 return { retry: true, delayMs: backoffDelay(attempt, random) };
778 }
779}
780
781function backoffDelay(attempt: number, random: () => number): number {
782 const base = RETRY_BASE_MS * Math.pow(2, attempt - 1);

Callers 1

requestWithMetaMethod · 0.85

Calls 1

backoffDelayFunction · 0.85

Tested by

no test coverage detected