From f0db21f3ffc255df1bc7c2d28f325d70e8247c0a Mon Sep 17 00:00:00 2001 From: Adrian Pascu Date: Tue, 28 Jul 2026 17:37:24 +0200 Subject: [PATCH] fix: reject server function calls on 5xx responses without X-Error --- .changeset/tidy-donkeys-reject.md | 5 +++ packages/start/src/fns/client.spec.ts | 61 +++++++++++++++++++++++++++ packages/start/src/fns/client.ts | 4 +- 3 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 .changeset/tidy-donkeys-reject.md create mode 100644 packages/start/src/fns/client.spec.ts diff --git a/.changeset/tidy-donkeys-reject.md b/.changeset/tidy-donkeys-reject.md new file mode 100644 index 000000000..2a43f23c8 --- /dev/null +++ b/.changeset/tidy-donkeys-reject.md @@ -0,0 +1,5 @@ +--- +"@solidjs/start": patch +--- + +Reject server function calls when the response is a 5xx without an X-Error header, instead of resolving with the parsed error body diff --git a/packages/start/src/fns/client.spec.ts b/packages/start/src/fns/client.spec.ts new file mode 100644 index 000000000..8054c4166 --- /dev/null +++ b/packages/start/src/fns/client.spec.ts @@ -0,0 +1,61 @@ +import { describe, expect, it, vi, beforeEach } from "vitest"; + +vi.mock("../shared/dev-toolbar/functions/tracker.ts", () => ({ + pushRequest: vi.fn(), + pushResponse: vi.fn(), +})); + +vi.mock("./serialization.ts", () => ({ + serializeToJSONString: vi.fn(async () => "[]"), +})); + +vi.mock("./shared.ts", async importOriginal => { + const actual = await importOriginal(); + return { ...actual, extractBody: vi.fn(async () => undefined) }; +}); + +const { cloneServerReference } = await import("./client.ts"); + +const respondWith = (status: number, headers: Record = {}) => { + vi.stubGlobal( + "fetch", + vi.fn(async () => new Response(null, { status, headers })), + ); +}; + +const callServerFunction = () => + (cloneServerReference("test-fn") as unknown as () => Promise)(); + +const rejectionOf = async (call: Promise) => { + try { + await call; + } catch (error) { + return error; + } + throw new Error("expected the server function call to reject"); +}; + +describe("fetchServerFunction", () => { + beforeEach(() => { + vi.stubEnv("BASE_URL", "http://localhost/"); + }); + + it("rejects when the response is a 5xx without an X-Error header", async () => { + respondWith(500); + const rejection = await rejectionOf(callServerFunction()); + expect(rejection).toBeInstanceOf(Error); + expect(rejection).toHaveProperty("message", "Server function call failed with status 500"); + }); + + it("rejects with an error when an X-Error response carries no body", async () => { + respondWith(403, { "X-Error": "true" }); + const rejection = await rejectionOf(callServerFunction()); + expect(rejection).toBeInstanceOf(Error); + expect(rejection).toHaveProperty("message", "Server function call failed with status 403"); + }); + + it("resolves normally for a successful response", async () => { + respondWith(200); + await expect(callServerFunction()).resolves.toBeUndefined(); + }); +}); diff --git a/packages/start/src/fns/client.ts b/packages/start/src/fns/client.ts index b3bf711a2..993febc3e 100644 --- a/packages/start/src/fns/client.ts +++ b/packages/start/src/fns/client.ts @@ -94,8 +94,8 @@ async function fetchServerFunction( } const result = await extractBody(instance, true, response.clone()); - if (response.headers.has("X-Error")) { - throw result; + if (response.headers.has("X-Error") || response.status >= 500) { + throw result ?? new Error(`Server function call failed with status ${response.status}`); } return result; }