Skip to content

fix(currency): a background rate refresh was breaking the render that started it - #814

Merged
github-actions[bot] merged 1 commit into
mainfrom
fix/rate-refresh-outside-render
Aug 28, 2026
Merged

fix(currency): a background rate refresh was breaking the render that started it#814
github-actions[bot] merged 1 commit into
mainfrom
fix/rate-refresh-outside-render

Conversation

@catomean

Copy link
Copy Markdown
Collaborator

Production logs, repeatedly:

Error: Page changed from static to dynamic at runtime /discover,
reason: revalidate: 0 fetch https://api.coingecko.com/api/v3/simple/price /discover

getCachedRateSnapshot is the "never wait on a third party to paint" path: it returns a snapshot or null and refreshes in the background, and nothing awaits that refresh. But not-awaited is not the same as not-attributed. fetchUpstream uses cache: 'no-store', Next tracks every fetch started during a render, and starting it synchronously put it inside the render's async context — so Next reclassified a statically prerendered route and the render failed.

A macrotask boundary moves the fetch out of that context. The refresh still happens, on the same schedule; only its attribution changes. Deliberately not after() from next/server: this module is called from plain server code as well as from requests, and must not require a request scope.

Also warms a cold process. With no snapshot at all — every deploy — the function returned null and scheduled nothing, so amounts stayed in BTC until some other caller happened to await getRateSnapshot(). Safe to schedule from here now that a refresh no longer contaminates the render that triggers it.

Honesty about scope

Found while chasing a timeline that renders its skeleton and never resolves. Whether this is that bug's cause is not established — the logged failures name /discover. But it is a real render-breaking fault in shared currency code, it fires on exactly the cold-start window where the timeline symptom is worst, and it is worth removing from the picture either way.

Proven by mutation: restoring the synchronous void refresh() fails three of the four timing tests.

🤖 Generated with Claude Code

https://claude.ai/code/session_012dpTLxh5GJWeWTF1UEvcD5

… started it

Production logs, repeatedly:

  Error: Page changed from static to dynamic at runtime /discover,
  reason: revalidate: 0 fetch https://api.coingecko.com/api/v3/simple/price /discover

getCachedRateSnapshot is the "never wait on a third party to paint" path:
it returns a snapshot or null and refreshes in the background, and nothing
awaits that refresh. But not-awaited is not the same as not-attributed.
fetchUpstream uses `cache: 'no-store'`, Next tracks every fetch started
during a render, and starting it synchronously put it inside the render's
async context — so Next reclassified a statically prerendered route and
the render failed.

A macrotask boundary moves the fetch out of that context. The refresh
still happens, on the same schedule; only its attribution changes.
Deliberately not `after()` from next/server: this module is called from
plain server code as well as from requests and must not require a request
scope.

Also warms a cold process. When there was no snapshot at all —
every deploy — the function returned null and scheduled NOTHING, so
amounts stayed in BTC until some other caller happened to await
getRateSnapshot(). Safe to schedule from here now that a refresh no
longer contaminates the render that triggers it.

Found while chasing a timeline that renders its skeleton and never
resolves. Whether this is that bug's cause is NOT established — the
logged failures name /discover — but it is a real render-breaking fault
on shared currency code, it fires on exactly the cold-start window where
the timeline symptom is worst, and the timing is worth removing from the
picture either way.

Proven by mutation: restoring the synchronous `void refresh()` fails
three of the four timing tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012dpTLxh5GJWeWTF1UEvcD5
@github-actions
github-actions Bot merged commit 3479c3d into main Aug 28, 2026
6 checks passed
@github-actions
github-actions Bot deleted the fix/rate-refresh-outside-render branch August 28, 2026 13:34
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.

1 participant