Files
cloudflare__vinext/tests/app-page-method.test.ts
T
Nathan Nguyen 16cb9ee5ef fix(app-router): return 405 for non-action mutations to static pages (#940)
* fix(app-router): return 405 for non-action mutations to static pages

Static and SSG App Router page routes currently render HTML or RSC payloads for POST and PUT requests that are not server actions. That diverges from Next.js and makes unsupported mutation methods look successful.

The App Router page render path only handled server actions and route handlers before falling through to page rendering. Add a small page-method policy helper and call it from the generated RSC entry before page rendering so static and SSG candidates return 405 with Allow: GET, HEAD while possible server action POSTs still pass through.

Tests cover the helper policy and the live App Router fixture for static and ISR pages.

* fix(app-router): preserve 405 allow header

Set the App Router page 405 Allow header after merging middleware response headers so middleware cannot clobber the methods advertised by the generated Method Not Allowed response.

Add a focused regression test that keeps middleware header propagation while asserting Allow remains GET, HEAD.
2026-04-28 20:37:49 +01:00

98 lines
2.9 KiB
TypeScript

import { describe, expect, it } from "vite-plus/test";
import {
isStaticOrSsgAppPageCandidate,
resolveAppPageMethodResponse,
} from "../packages/vinext/src/server/app-page-method.js";
describe("app page method policy", () => {
it("returns 405 with Allow for non-action mutation requests to static candidates", async () => {
const response = resolveAppPageMethodResponse({
hasGenerateStaticParams: false,
isDynamicRoute: false,
request: new Request("https://example.com/about", { method: "POST" }),
revalidateSeconds: null,
});
if (!response) {
throw new Error("Expected a Method Not Allowed response");
}
expect(response.status).toBe(405);
expect(response.headers.get("allow")).toBe("GET, HEAD");
await expect(response.text()).resolves.toBe("Method Not Allowed");
});
it("preserves possible server action POSTs", () => {
const response = resolveAppPageMethodResponse({
hasGenerateStaticParams: false,
isDynamicRoute: false,
request: new Request("https://example.com/about", {
headers: { "next-action": "abc123" },
method: "POST",
}),
revalidateSeconds: null,
});
expect(response).toBeNull();
});
it("does not let middleware headers override the 405 Allow header", () => {
const middlewareHeaders = new Headers({
Allow: "POST",
"x-from-middleware": "1",
});
const response = resolveAppPageMethodResponse({
hasGenerateStaticParams: false,
isDynamicRoute: false,
middlewareHeaders,
request: new Request("https://example.com/about", { method: "PUT" }),
revalidateSeconds: null,
});
if (!response) {
throw new Error("Expected a Method Not Allowed response");
}
expect(response.headers.get("allow")).toBe("GET, HEAD");
expect(response.headers.get("x-from-middleware")).toBe("1");
});
it("treats ISR and generateStaticParams routes as SSG candidates", () => {
expect(
isStaticOrSsgAppPageCandidate({
hasGenerateStaticParams: false,
isDynamicRoute: false,
revalidateSeconds: 60,
}),
).toBe(true);
expect(
isStaticOrSsgAppPageCandidate({
hasGenerateStaticParams: true,
isDynamicRoute: true,
revalidateSeconds: null,
}),
).toBe(true);
});
it("does not guard force-dynamic or revalidate zero pages", () => {
expect(
resolveAppPageMethodResponse({
dynamicConfig: "force-dynamic",
hasGenerateStaticParams: false,
isDynamicRoute: false,
request: new Request("https://example.com/dynamic", { method: "PUT" }),
revalidateSeconds: null,
}),
).toBeNull();
expect(
resolveAppPageMethodResponse({
hasGenerateStaticParams: false,
isDynamicRoute: false,
request: new Request("https://example.com/no-store", { method: "PUT" }),
revalidateSeconds: 0,
}),
).toBeNull();
});
});