feat(portal-client): retry transient null heads in getHead/getFinalizedHead - #523
Merged
Merged
Conversation
…edHead The portal answers /head and /finalized-head with a `200` + `null` body for a short window while a dataset's (finalized) head is not yet established — e.g. right after a store restart/cold-start. Consumers that require a head (e.g. evm-stream's `PortalEvmDataSource.getFinalizedHead`) then crash on the resulting `undefined`. Add a short, finite backoff (default `[100, 500, 1000, 2000]` ms) in `getHead` (covering `getFinalizedHead`) that retries only the null-body case; transport errors remain handled by the underlying HttpClient. Once the schedule is exhausted the call resolves to `undefined`, so persistently head-less datasets don't hang. Configurable via `PortalClientOptions.headRetrySchedule` with a per-request override. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XmEtzbcoThPGFsY59qjTai
Contributor
There was a problem hiding this comment.
Pull request overview
Adds a finite, abort-aware backoff retry to @subsquid/portal-client’s getHead / getFinalizedHead to tolerate a transient “no head yet” response from the portal, and introduces a small Vitest suite to validate the behavior.
Changes:
- Add configurable
headRetrySchedule(client-level default + per-request override) and retry loop for transient null heads ingetHead/getFinalizedHead. - Add unit tests covering retry success, exhaustion behavior, opt-out, and abort during backoff.
- Enable package-level testing by adding a
testscript and wiring Vitest into the package and Rush lock/state.
Reviewed changes
Copilot reviewed 4 out of 5 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| util/portal-client/src/client.ts | Implements head retry schedule option and backoff retry loop for transient null heads. |
| util/portal-client/src/client.test.ts | Adds Vitest coverage for retry/backoff and abort behavior. |
| util/portal-client/package.json | Adds test script and Vitest devDependency for the new tests. |
| common/config/rush/repo-state.json | Updates Rush shrinkwrap hash after dependency changes. |
| common/config/rush/pnpm-lock.yaml | Lockfile updates reflecting Vitest wiring and resulting dependency graph changes. |
Files not reviewed (1)
- common/config/rush/pnpm-lock.yaml: Generated file
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…opilot #523) Narrow the head retry trigger to a JSON `null` body (`res.body === null`) — the portal's actual "no head yet" signal — instead of any falsy `res.body`, so an empty/absent body (`undefined`) returns immediately rather than incurring the full backoff. Reword the JSDoc from "empty body" to "JSON `null` body" to match real HttpClient behavior, and add a test asserting an `undefined` body is not retried. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XmEtzbcoThPGFsY59qjTai
abernatskiy
added a commit
that referenced
this pull request
Jul 8, 2026
The merged PR #523 (retry transient null heads) shipped without a change description; add it so the next version bump includes the feature. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XmEtzbcoThPGFsY59qjTai
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.
Problem
/headand/finalized-headreturn a200with anullbody for a short window while a dataset's (finalized) head is not yet established — e.g. right after a store restart/cold-start (the store'sget_finalized_head()is transientlyNone, and network-only datasets whose network head isn't tracked yet hit the same path).PortalClient.getHeadturns thatnullintoundefined, and consumers that require a head crash on it. Observed in the wild as:The null is transient — the same dataset returns a real head moments later (once the replica warms up / finality is tracked). It's rare, intermittent, and not something the caller should hard-fail on.
Change
Add a short, finite retry with backoff to
getHead(whichgetFinalizedHeaddelegates to):HttpClient— untouched.[100, 500, 1000, 2000]ms (~3.6 s, 5 attempts). Sized to the transient window: the first short step catches load-balancer reselection onto a warm replica; the later steps cover a brief cold-start. Finite by design — once exhausted the call resolves toundefined, so persistently head-less datasets (e.g. network-only ones with no head) don't hang; the longer horizon stays with the caller's existing poll/throttle loop.options.abortduring backoff).PortalClientOptions.headRetrySchedule, with a per-request override onPortalRequestOptions. An empty array opts out.No signature or type changes —
getHead/getFinalizedHeadstill resolve toBlockRef | undefined.Tests
util/portal-client/src/client.test.tscovers: retry-then-success, exhaustion →undefined(exact request count), empty-schedule opt-out, immediate return on a real head, and abort during backoff.🤖 Generated with Claude Code