Repository navigation
Retry engine and error classification: instanceof IoError after the flat tier, non-finite delayOverride, backoff NaN #78
Copy link
Copy link
Closed
Labels
area:coreCore HTTP, IO, body, context, encoding: HTTP-* IO-* BODY-* CTX-* UTF-*Core HTTP, IO, body, context, encoding: HTTP-* IO-* BODY-* CTX-* UTF-*area:resilienceRetry, recovery, redirects: RETRY-* RECOV-* REDIR-*Retry, recovery, redirects: RETRY-* RECOV-* REDIR-*type:bugDefect in shipped behaviorDefect in shipped behavior
Milestone
Description
Part of the audit remediation umbrella #67. Milestone 4. Severity: MEDIUM. Requirements: RETRY-2, RETRY-11, RETRY-40, §10 item 17.
Current SDK behavior
- The cause-walk misses four I/O leaves.
classify.ts:73narrows with
current instanceof IoError. After the 2026-09-04 tier flattening,EndOfStreamError,
SourceContractViolationError,ClosedResourceErrorandAllocationLimitError
(io/errors.ts:29,51,67,83) extendDexpaceErrordirectly and are grouped byisIoError.
OnlyIoErroritself andTransportFailureErrormatch. A truncated response body
(EndOfStreamError) is not retryable; a no-response transport failure is.
docs/deviations.mditem 17 gives "the cause-walk returns retryable for anyIoError" as the
reasonTransportFailureErrormust extendIoError. - A non-finite
delayOverrideis fatal and masks the real failure.
resolveDelay(retry/engine.ts:127-138) does not validate the override result.
overshootsBudget(NaN)is false,waitFor'sdelayMs <= 0guard (:294) is false,
Clock.sleeprejects withRangeError, and:416-422folds it into the terminal failure.
delayOverride: () => NaNwithmaxAttempts: 3gives one send and surfaces
RangeError: Clock.sleep: durationMs must be a non-negative finite number, got NaN, with the
realTransportFailureErrorin.suppressed. A throwing override correctly gives three
sends (RETRY-40). - Backoff is
NaNwheninitialDelayMsis 0 and the power overflows.
backoff.ts:74:0 * Infinity.retrySettings({initialDelayMs: 0, multiplier: 1e200, maxAttempts: 4})
givescomputeDelay(3) === NaN; so does{initialDelayMs: 0, multiplier: 2, maxAttempts: 2000}
at attempt 1100. The comment at:49-50assumesinitialDelayMs > 0.
Expected behavior
The retry classifier has one stated rule for I/O failures, and each of the five I/O error
classes has a test that pins whether it is retryable. A delayOverride that returns a
non-finite value is treated like one that throws: the computed backoff is used and the
attempt loop continues. computeDelay returns a finite non-negative number for every
accepted setting and attempt count.
Notes and leads
instanceof IoErroris a decision. (a) Switch toisIoError(current)and decide per leaf;
AllocationLimitErrorandClosedResourceErrorshould probably not retry; an
EndOfStreamErrorraised while writing a request body means bytes went out, so the engine's
replayability gate must hold. (b) Keepinstanceof IoErroras "only transport-layer failures
retry" and rewrite item 17 andio/index.tsto say so. Either way, five classify tests.resolveDelay:Number.isFinite(override) && override >= 0 ? override : computed.computeDelay: short-circuitinitialDelayMs === 0to0, orNumber.isFiniteguard
beforeMath.min.- Subtask 5 (typed error surfaced) touches
engine.tstoo; land that first to avoid a
conflict inwithTrail. - Patch changeset for
@dexpace/core. Updatedocs/deviations.mditem 17.
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
area:coreCore HTTP, IO, body, context, encoding: HTTP-* IO-* BODY-* CTX-* UTF-*Core HTTP, IO, body, context, encoding: HTTP-* IO-* BODY-* CTX-* UTF-*area:resilienceRetry, recovery, redirects: RETRY-* RECOV-* REDIR-*Retry, recovery, redirects: RETRY-* RECOV-* REDIR-*type:bugDefect in shipped behaviorDefect in shipped behavior
Activity
docs(retry): RetryStepOptions.delayOverride states the failure semantics
Wahbeh-Mohammad commentedon Sep 5, 2026
Addressed in PR #93, merged into
audit/remediation-67; reachesmvpthrough PR #98. Decision record:docs/audit-67-decisions.md.