Repository navigation
Conversation
A server component that throws during render ships its error as the sanitized message string. `landing()` told an error it had already surfaced from the next flight's error by comparing payloads, so a re-asked flight that failed the same way looked like the old error and re-asked again: one request per flight with no end (about 3000/s in the hackernews example after a hover preload of a failing route). Key the surfaced error by the response version that carried it. Each flight is a new version, so `reset()` and a fresh mount over an errored address make one request and the error reaches the nearest `<Errored>` again (or halts, with none). Co-authored-by: Claude via Cursor <noreply@cursor.com> Co-authored-by: Cursor <cursoragent@cursor.com>
🦋 Changeset detectedLatest commit: 87c0068 The changes in this PR will be included in the next version bump. This PR includes changesets to release 12 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Size (brotli, eager entry chunk)
|
Coverage Report for CI Build 37806617613Warning Build has drifted: This PR's base is out of sync with its target branch, so coverage data may include unrelated changes. Coverage remained the same at 76.43%Details
Uncovered ChangesNo uncovered changes found. Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
Fold the frame read, the version key and the surfaced check into one helper that returns false / 1 (already surfaced: re-ask) / 2 (new: throw), and let reask() chain the next landing itself. Same behaviour as the version-keyed fix; 5 B smaller minified than next instead of +56 B. Co-authored-by: Cursor <cursoragent@cursor.com>
…ypes Co-authored-by: Cursor <cursoragent@cursor.com>
Problem
A server component that throws during render ships its error as the sanitized message string (
sink.error("", message)insrc/server.tsandframes/src/frame-sink.ts). The frames client'slanding()decided whether an error was one it had already surfaced by comparing the error value. Two flights that fail the same way carry equal strings, so a re-asked flight's error looked like the old one and the node re-asked again. That flight's error record bumped the mount'sfailedtick, the memo recomputed, and it re-asked again, with no end.It shows up whenever a re-ask meets a repeatable server error:
<Errored>reset()on a component that keeps failing;/users/nulllink and then clicking it produced about 3000 requests per second, and the navigation never committed.The existing reset spec missed it because its fixtures send
{ message }objects, and a freshly parsed object never compares equal.Fix
landing()keys the surfaced error by the response version that carried it, read off the frame bound to the address, instead of by the payload. Every flight lands under a new version, so a re-asked flight's error is new however alike, while a re-read of the error already thrown (areset, or a fresh consumer of an errored address) is still a re-ask. What<Errored>receives is unchanged.Behaviour changes
<Errored>(or halts withREACTIVITY_HALTEDwhen there is none) instead of looping.Public API changes
None.
landing()is internal to@solidjs/web/frames.Tests
New
packages/web/test/frames-reask-loop.spec.tsx, using the server's real string payload. The fetch stub stops answering past 25 calls so a regression fails at a readable count instead of exhausting memory. The cases:reset()re-asks once per reset, and the equal error surfaces again (viadynamicanddynamicComponent);<Errored>, the same mount halts instead of re-asking.All five fail at the cap without the fix. The full client web suite passes.
I also checked it end to end in the hackernews example (with #3910 applied, which that example needs). After the hover and click, the request count settles at the route's call plus one re-ask.
Follow-ups (not in this PR)
<Errored>as a string, but transport failures (and keyed fragment errors) as{ message }objects. Aligning them would be a public behaviour change.{ message }"the server's own shape"; the server sends a string.Size
The first version keyed the error through a helper that read
frame.versionand re-read the frame to throw, which cost +56 B minified per page. The trim folds the frame read, the version key and the surfaced check into one helper that returnsfalse(no error),1(already surfaced, so re-ask) or2(new, so throw).reask()now chains the next landing itself. Behaviour is unchanged, and the change is now 5 B smaller minified thannext. No cap or exception changes.Bytes, measured locally with the CI harness after merging
next(a963ec1). Minified is deterministic; brotli moves by tens of bytes with layout.nextmin / brThe two compiled pages are over their brotli caps on brotli noise. They pass on the minified allowance (
page: compiled base server componentsis already over onnext, by 16 B). In CI (merged with a newernext)page: live + routermeasures 142,467 B minified and 11 B over its brotli cap, also within the allowance (5 B under its recorded minified). The size check passes with warnings only.