Skip to content

Commit ec52360

Browse files
fix(web): contain flight-data collector errors per source
Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 5be07a8 commit ec52360

3 files changed

Lines changed: 47 additions & 2 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@solidjs/web": patch
3+
---
4+
5+
Contain flight-data collector errors per source: a throwing collector no longer fails the mutation response (the client received an error for a mutation that succeeded) or drop the other sources' slices — the failing source is simply omitted and logged.

packages/web/server-functions/src/server.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1025,8 +1025,17 @@ async function foldFlightData(hooks, event, headers, outcome, context = {}) {
10251025
digestOutcome(event, outcome);
10261026
const folded = [];
10271027
for (const [source, hook] of hooks) {
1028-
const slice = await hook(event, outcome);
1029-
if (slice !== undefined) folded.push([source, slice]);
1028+
// Contained per source: one cache's collector failing must not cost the
1029+
// mutation's outcome or the other caches' slices — without this, a
1030+
// thrown hook falls into the handler's outer catch and the client
1031+
// receives an ERROR for a mutation that succeeded. A missing slice just
1032+
// means that cache revalidates the normal way.
1033+
try {
1034+
const slice = await hook(event, outcome);
1035+
if (slice !== undefined) folded.push([source, slice]);
1036+
} catch (error) {
1037+
console.error(`Error collecting flight data for source "${source}"`, error);
1038+
}
10301039
}
10311040
if (folded.length === 0) return outcome.value;
10321041
const legacy = folded.length === 1 && folded[0][0] === "true";

packages/web/test/server/server-functions-single-flight.spec.tsx

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,37 @@ describe("single-flight server bridge (built server bundle)", () => {
163163
}
164164
});
165165

166+
it("a throwing collector loses only its own slice", async () => {
167+
registerServerFunction("sf-bridge-throw-0", async () => "mutated");
168+
const unregisterBroken = registerFlightDataSource("broken", () => {
169+
throw new Error("collector exploded");
170+
});
171+
const unregisterQuery = registerFlightDataSource("sq", () => ({ queries: ["fresh"] }));
172+
const consoleError = vi.spyOn(console, "error").mockImplementation(() => {});
173+
try {
174+
const response = await handleServerFunctionRequest(
175+
flightRequest("sf-bridge-throw-0", "broken,sq")
176+
);
177+
// The mutation's outcome and the healthy cache's slice both survive —
178+
// a thrown hook must not fall into the handler's error path (the
179+
// client would receive an error for a mutation that succeeded) or
180+
// take the other sources down with it.
181+
expect(response.headers.get(SINGLE_FLIGHT_HEADER)).toBe("sq");
182+
expect(await decodeResponse(response)).toEqual({
183+
value: "mutated",
184+
data: { sq: { queries: ["fresh"] } }
185+
});
186+
expect(consoleError).toHaveBeenCalledWith(
187+
expect.stringContaining('"broken"'),
188+
expect.any(Error)
189+
);
190+
} finally {
191+
consoleError.mockRestore();
192+
unregisterBroken();
193+
unregisterQuery();
194+
}
195+
});
196+
166197
it("an unrecognized opt-in value still reaches the unnamed hook", async () => {
167198
// Hand-tagged requests from integrations predating named sources sent
168199
// arbitrary truthy values; any of them must keep opting in.

0 commit comments

Comments
 (0)