Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 33 additions & 4 deletions packages/vinext/src/server/server-action-not-found.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,44 @@ export function isServerActionNotFoundError(error: unknown, actionId: string | n
return false;
}

if (!actionId) {
return message.startsWith("Failed to find Server Action");
if (actionId && message.startsWith(getServerActionNotFoundPrefix(actionId))) {
return true;
}

if (message.startsWith(getServerActionNotFoundPrefix(actionId))) {
if (!actionId && message.startsWith("Failed to find Server Action")) {
return true;
}

return Boolean(actionId && message.includes(`[vite-rsc] invalid server reference '${actionId}'`));
// `@vitejs/plugin-rsc` raises two different "no such server reference"
// errors depending on the build mode. Both mean the same thing — the
// referenced server action id isn't in the runtime manifest — and must
// surface as Next.js' 404 + action-not-found header rather than a generic
// 500. The progressive (no-JS) path also hits this in `decodeAction(body)`
// before it has any actionId in hand, so match these patterns whether or
// not the caller has resolved an action id from request headers.
//
// - dev: `[vite-rsc] invalid server reference '<id>'` (from the reference
// validation virtual module loaded ahead of dynamic import)
// - prod: `server reference not found '<id>'` (from the built
// `virtual:vite-rsc/server-references` lookup, including the case
// where the build has no server actions at all)
//
// See: @vitejs/plugin-rsc dist/rsc.js (`server reference not found`) and
// dist/plugin-*.js (`[vite-rsc] invalid <type> reference`).
if (actionId) {
if (message.includes(`[vite-rsc] invalid server reference '${actionId}'`)) {
return true;
}
if (message.includes(`server reference not found '${actionId}'`)) {
return true;
}
return false;
}

return (
/\[vite-rsc] invalid server reference '/.test(message) ||
/server reference not found '/.test(message)
);
}

export function createServerActionNotFoundResponse(): Response {
Expand Down
54 changes: 54 additions & 0 deletions tests/app-server-action-execution.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -583,6 +583,26 @@ describe("app server action execution helpers", () => {
expect(clearContext).toHaveBeenCalledTimes(1);
});

// The progressive (MPA / no-JS form POST) path also needs to recognise the
// prod-build "server reference not found" shape thrown by
// `@vitejs/plugin-rsc` when the referenced action id isn't in the built
// manifest, including when the build has no server actions at all.
it("returns action-not-found when the prod build has no matching reference on a progressive action", async () => {
const response = requireProgressiveActionResponse(
await handleProgressiveServerActionRequest(
createOptions({
async decodeAction() {
throw new Error("server reference not found 'abc123'");
},
}),
),
);

expect(response.status).toBe(404);
expect(response.headers.get("x-nextjs-action-not-found")).toBe("1");
expect(await response.text()).toBe("Server action not found.");
});

it("returns action-not-found for progressive decode misses that include an action id", async () => {
const response = requireProgressiveActionResponse(
await handleProgressiveServerActionRequest(
Expand Down Expand Up @@ -878,6 +898,40 @@ describe("app server action execution helpers", () => {
expect(decodeReply).not.toHaveBeenCalled();
});

// Reproduces the prod-build error path where the @vitejs/plugin-rsc server
// references manifest doesn't include the requested action id. In a build
// with NO server actions defined at all, the action loader throws
// `server reference not found '<id>'` (not the dev-mode `[vite-rsc] invalid
// server reference '<id>'`). Both shapes have to land on the 404 +
// `x-nextjs-action-not-found` response that the client router recognises.
//
// Ported from Next.js: test/e2e/app-dir/no-server-actions/no-server-actions.test.ts
// https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/no-server-actions/no-server-actions.test.ts
it("returns action-not-found when the prod build has no matching server reference", async () => {
const reportedErrors: Error[] = [];
const renderToReadableStream = vi.fn();

const response = await handleServerActionRscRequest(
createRscOptions({
actionId: "abc123",
loadServerAction() {
return Promise.reject(new Error("server reference not found 'abc123'"));
},
renderToReadableStream,
reportRequestError(error) {
reportedErrors.push(error);
},
}),
);

expect(response?.status).toBe(404);
expect(response?.headers.get("x-nextjs-action-not-found")).toBe("1");
expect(response?.headers.get("content-type")).toBe("text/plain");
expect(await response?.text()).toBe("Server action not found.");
expect(reportedErrors).toEqual([]);
expect(renderToReadableStream).not.toHaveBeenCalled();
});

it("keeps unrelated server action loader failures on the generic error path", async () => {
const reportedErrors: Error[] = [];
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
Expand Down
Loading