Repository navigation
With serverFunctions.components on, data server functions called during hydration never reach the network (frames intercept waits forever) #4033
Description
In an app with serverFunctions: { components: true }, a data server function (one that returns data, not a component) called during hydration never sends its request. The frames client's intercept parks the call waiting for a <solid-frame> boundary that a data function never has, and in two ways that wait never ends.
examples/room's /live page shows it on next (4937f0a):
- Never connects: there are no
/_server/live/*or/_server/data/summaryrequests at all, and no console errors. - UI stuck: the status pills stay at "connecting", presence shows "0 here" and "Not in the room yet", and the summary stays at "summarizing…".
- Writes go one way: a message posted from one tab reaches the server but never shows in another tab.
- Everywhere: it happens in dev over HTTP/1.1, in dev over HTTPS/HTTP/2, and in the production build, while
_$HY.done === true. - Controls: room's
/page, a live server component, works on the same server. The same/livesource served withcomponents: trueremoved connects, with both tabs in presence and posts crossing.examples/boarddoesn't setcomponents: true, which is why it connects on the same build.
This is related to #3819, which uses the same page as its motivating case, but that issue is about the connection opening late. Here it never opens.
Cause
In packages/web/frames/src/client.ts, the intercept passed to createServerComponentHandler (around line 1683) runs for every server-function call, data functions included. While boundaryMayArrive() is true, meaning _$HY.done is unset or a fragment is pending, it treats a function with no boundary element as "a boundary that may still arrive" and returns awaitBoundary(id). That is a promise cached per function id in arrivals, and the call doesn't go to the network. A data function never gets a boundary, so it can only be released by the reveal hook. Two defects keep that release from ever happening.
1. The cached arrival promise can be a MockPromise that never resolves (confirmed)
- The first call happens inside the hydration trace (
subFetchinpackages/solid/src/client/hydration.ts), which swaps the globalPromiseforMockPromise. - So
new Promise(r => (resolve = r))inawaitBoundarycreates a MockPromise andresolvestays undefined. - In the browser, the
arrivalsentries for presence, transcript, roomCard and archive all hadtypeof resolve === "undefined", created exactly whenPromise !== native. - The takeover calls after the root pass hit the same cached dead entry and hang.
- If a reveal ever tries to release them, it throws
TypeError: arrival.resolve is not a function. That throw was reproduced.
This is the same class of bug as #4013 (a lazy() loader promise made under hydration tracing). That fix added a tracing check, and frames doesn't consult it.
2. Waiters registered after the page's last reveal are never released (confirmed)
The summary source hangs even though its arrival was created with a real Promise:
- Waiters are released only from a reveal notification, and only if
hy.done && !hy.fr.pending()holds at that moment. - Timeline: reveals at about 520 ms and 720 ms, then the last one (the 4 s Archive panel) at about 4015 ms, with
donestill unset andpending()false. _$HY.done = trueis set about 5 ms later, from asetTimeoutindrainHydrationCallbacks. So the last reveal kept the waiters, and no reveal comes after it.- Calls made by hydration-end recomputes (summary uses
ssrSource: "client") register in that gap and wait forever. - Firing a synthetic reveal after
done(with the dead MockPromise entries dropped first) immediately sentGET /_server/data/summary, and the summary started streaming.
A fresh call after hydration goes straight to the network, because boundaryMayArrive() is false once done is set. Only calls made in the hydration window are lost, and any components: true app that calls data server functions during hydration should hit this.
Possible fixes (untested)
- Don't defer non-component functions in the intercept: a data function can never have a boundary.
- Don't create or cache arrival promises while hydration tracing is on, as Hydration tracing permanently caches a mocked promise in lazy().preload() #4013's fix does for lazy().
- Release outstanding waiters at hydration end (
onHydrationEnd) as well as on reveals.
Regression?
Unknown. The intercept, awaitBoundary and boundaryMayArrive code predates the held-swap policy note (#2964) in the comments, and an older build (around 06c7717) behaves the same.
Activity
Sravan1011 commentedon Oct 14, 2026
@ryansolid can I try this out ? solve this
fix(frames): data calls made during hydration reach the network (#4033)
ryansolid commentedon Oct 14, 2026
Generated by Claude Code