Skip to content

feat(portal-client): retry transient null heads in getHead/getFinalizedHead - #523

Merged
abernatskiy merged 2 commits into
masterfrom
feat/portal-client-retry-null-head
Jul 8, 2026
Merged

feat(portal-client): retry transient null heads in getHead/getFinalizedHead#523
abernatskiy merged 2 commits into
masterfrom
feat/portal-client-retry-null-head

Conversation

@abernatskiy

Copy link
Copy Markdown
Contributor

Problem

/head and /finalized-head return a 200 with a 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 (the store's get_finalized_head() is transiently None, and network-only datasets whose network head isn't tracked yet hit the same path).

PortalClient.getHead turns that null into undefined, and consumers that require a head crash on it. Observed in the wild as:

AssertionError: portal has no finalized head
  at PortalEvmDataSource.getFinalizedHead (@subsquid/evm-stream/lib/portal/source.js:37)

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 (which getFinalizedHead delegates to):

  • Retries only the null-body case. Transport errors (connection/timeout/5xx) are already retried by the underlying HttpClient — untouched.
  • Default schedule [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 to undefined, 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.
  • Abort-aware (honours options.abort during backoff).
  • Configurable via PortalClientOptions.headRetrySchedule, with a per-request override on PortalRequestOptions. An empty array opts out.

No signature or type changes — getHead/getFinalizedHead still resolve to BlockRef | undefined.

Tests

util/portal-client/src/client.test.ts covers: 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

…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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 in getHead/getFinalizedHead.
  • Add unit tests covering retry success, exhaustion behavior, opt-out, and abort during backoff.
  • Enable package-level testing by adding a test script 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.

Comment thread util/portal-client/src/client.ts
Comment thread util/portal-client/src/client.ts Outdated
…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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 5 changed files in this pull request and generated no new comments.

Files not reviewed (1)
  • common/config/rush/pnpm-lock.yaml: Generated file

@abernatskiy
abernatskiy merged commit cb1f41d into master Jul 8, 2026
3 checks passed
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants