Skip to content

With serverFunctions.components on, data server functions called during hydration never reach the network (frames intercept waits forever) #4033

Closed

Description

@ryansolid

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/summary requests 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 /live source served with components: true removed connects, with both tabs in presence and posts crossing. examples/board doesn't set components: 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 (subFetch in packages/solid/src/client/hydration.ts), which swaps the global Promise for MockPromise.
  • So new Promise(r => (resolve = r)) in awaitBoundary creates a MockPromise and resolve stays undefined.
  • In the browser, the arrivals entries for presence, transcript, roomCard and archive all had typeof resolve === "undefined", created exactly when Promise !== 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 done still unset and pending() false.
  • _$HY.done = true is set about 5 ms later, from a setTimeout in drainHydrationCallbacks. 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 sent GET /_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)

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

  1. Sravan1011 commented on Oct 14, 2026

    @Sravan1011

    @ryansolid can I try this out ? solve this

  2. ryansolid commented on Oct 14, 2026

    @ryansolid
    MemberAuthor

    Fixed by #4055 (commit 8163bc3), which is merged into next. Data server functions called during hydration with components: true now reach the network instead of waiting forever in the frames intercept. data-call-during-hydration-4033.spec.tsx covers it. This issue didn't close automatically because next isn't the default branch.

    — Claude via Claude Code


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions