Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export async function GET(
request: Request,
{ params }: { params: Promise<{ slug: string }> },
) {
const { slug } = await params;
return Response.json({ slug });
}
10 changes: 10 additions & 0 deletions examples/app-router/app/methods/get/query/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { NextRequest } from "next/server";

export function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const query = searchParams.get("query");
if (query === "OpenNext is awesome!") {
return Response.json({ query });
}
return new Response("Internal Server Error", { status: 500 });
}
5 changes: 5 additions & 0 deletions examples/app-router/app/methods/get/redirect/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { redirect } from "next/navigation";

export async function GET(request: Request) {
redirect("https://nextjs.org/");
}
10 changes: 10 additions & 0 deletions examples/app-router/app/methods/get/revalidate/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export const revalidate = 5;

async function getTime() {
return new Date().toISOString();
}

export async function GET() {
const time = await getTime();
return Response.json({ time });
}
10 changes: 10 additions & 0 deletions examples/app-router/app/methods/get/static/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export const dynamic = "force-static";

async function getTime() {
return new Date().toISOString();
}

export async function GET() {
const time = await getTime();
return Response.json({ time });
}
19 changes: 19 additions & 0 deletions examples/app-router/app/methods/post/cookies/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { cookies } from "next/headers";

export async function POST(request: Request) {
const formData = await request.formData();
const username = formData.get("username");
const password = formData.get("password");
if (username === "hakuna" && password === "matata") {
(await cookies()).set("auth_session", "SUPER_SECRET_SESSION_ID_1234");
return Response.json(
{
message: "ok",
},
{
status: 202,
},
);
}
return Response.json({ message: "you must login" }, { status: 401 });
}
16 changes: 16 additions & 0 deletions examples/app-router/app/methods/post/formdata/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export async function POST(request: Request) {
const formData = await request.formData();
const name = formData.get("name");
const email = formData.get("email");
if (name === "OpenNext [] () %&#!%$#" && email === "[email protected]") {
return Response.json(
{
message: "ok",
},
{
status: 202,
},
);
}
return Response.json({ message: "forbidden" }, { status: 403 });
}
76 changes: 76 additions & 0 deletions examples/app-router/app/methods/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import type { NextRequest } from "next/server";

export async function GET() {
return Response.json({
message: "OpenNext is awesome! :) :] :> :D",
});
}

export async function POST(request: Request) {
const text = await request.text();
if (text === "OpenNext is awesome! :] :) :> :D") {
return Response.json(
{
message: "ok",
},
{
status: 202,
},
);
}
return Response.json({ message: "forbidden" }, { status: 403 });
}

export async function PUT(request: Request) {
const res = (await request.json()) as {
message: string;
};
if (res.message === "OpenNext PUT") {
return Response.json({ message: "ok" }, { status: 201 });
}
return Response.json({ message: "error" }, { status: 500 });
}

export async function PATCH(request: Request) {
const res = (await request.json()) as {
message: string;
};
if (res.message === "OpenNext PATCH") {
return Response.json(
{ message: "ok", modified: true, timestamp: new Date().toISOString() },
{ status: 202 },
);
}
return Response.json({ message: "error" }, { status: 500 });
}

export async function DELETE(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const command = searchParams.get("command");
if (command === "rm -rf / --no-preserve-root") {
return new Response(null, { status: 204 });
}
return Response.json({ message: "error" }, { status: 500 });
}

export async function HEAD() {
return new Response("hello", {
status: 200,
headers: {
"content-type": "text/html; charset=utf-8",
// Once deployed to AWS this will always be 0
// "content-length": "1234567",
"special-header": "OpenNext is the best :) :] :> :D",
},
});
}

export async function OPTIONS() {
return new Response(null, {
status: 204,
headers: {
Allow: "GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, LOVE",
Special: "OpenNext is the best :) :] :> :D",
},
});
}
188 changes: 188 additions & 0 deletions packages/tests-e2e/tests/appRouter/methods.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import { expect, test } from "@playwright/test";

test.describe("all supported methods should work in route handlers", () => {
test("GET", async ({ request }) => {
const getRes = await request.get("/methods");
const getData = await getRes.json();
expect(getRes.status()).toEqual(200);
expect(getData.message).toEqual("OpenNext is awesome! :) :] :> :D");
});

test("POST", async ({ request }) => {
const postRes = await request.post("/methods", {
headers: {
"Content-Type": "text/plain",
},
data: "OpenNext is awesome! :] :) :> :D",
});
expect(postRes.status()).toBe(202);
const postData = await postRes.json();
expect(postData.message).toBe("ok");
const errorPostRes = await request.post("/methods", {
headers: {
"Content-Type": "text/plain",
},
data: "OpenNext is not awesome! :C",
});
expect(errorPostRes.status()).toBe(403);
const errorData = await errorPostRes.json();
expect(errorData.message).toBe("forbidden");
});

test("PUT", async ({ request }) => {
const putRes = await request.put("/methods", {
data: {
message: "OpenNext PUT",
},
});
expect(putRes.status()).toEqual(201);
const putData = await putRes.json();
expect(putData.message).toEqual("ok");
});

test("PATCH", async ({ request }) => {
const timestampBefore = new Date();
const patchRes = await request.patch("/methods", {
data: { message: "OpenNext PATCH" },
});
expect(patchRes.status()).toEqual(202);
const patchData = await patchRes.json();
expect(patchData.message).toEqual("ok");
expect(patchData.modified).toEqual(true);
expect(Date.parse(patchData.timestamp)).toBeGreaterThan(
timestampBefore.getTime(),
);
});

test("DELETE", async ({ request }) => {
const deleteRes = await request.delete("/methods", {
params: {
command: "rm -rf / --no-preserve-root",
},
});
expect(deleteRes.status()).toEqual(204);
});

test("HEAD", async ({ request }) => {
const headRes = await request.head("/methods");
expect(headRes.status()).toEqual(200);
const headers = headRes.headers();
expect(headers["content-type"]).toEqual("text/html; charset=utf-8");
// expect(headers["content-length"]).toEqual("1234567");
expect(headers["special-header"]).toEqual(
"OpenNext is the best :) :] :> :D",
);
});

test("OPTIONS", async ({ request }) => {
const optionsRes = await request.fetch("/methods", {
method: "OPTIONS",
});
expect(optionsRes.status()).toEqual(204);
const headers = optionsRes.headers();
expect(headers.allow).toBe(
"GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, LOVE",
);
expect(headers.special).toBe("OpenNext is the best :) :] :> :D");
});
});

test("formData should work in POST route handler", async ({ request }) => {
const formData = new FormData();
formData.append("name", "OpenNext [] () %&#!%$#");
formData.append("email", "[email protected]");
const postRes = await request.post("/methods/post/formdata", {
form: formData,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Posting a FormData doesn't work for Cloudflare, posting an object is fine (as in the cookies test). Wondering why it works here?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nvm updating Playwright fixes this

});
expect(postRes.status()).toBe(202);
const postData = await postRes.json();
expect(postData.message).toBe("ok");
});

test("revalidate should work in GET route handler", async ({
request,
page,
}) => {
let time = Date.parse(
(await request.get("/methods/get/revalidate").then((res) => res.json()))
.time,
);
let newTime: number;
let tempTime = time;
do {
await page.waitForTimeout(1000);
time = tempTime;
const newTimeRes = await request.get("/methods/get/revalidate");
newTime = Date.parse((await newTimeRes.json()).time);
tempTime = newTime;
} while (time !== newTime);
const midTime = Date.parse(
(await request.get("/methods/get/revalidate").then((res) => res.json()))
.time,
);

await page.waitForTimeout(1000);
// Expect that the time is still stale
expect(midTime).toEqual(newTime);

// Wait 5 + 1 seconds for ISR to regenerate time
await page.waitForTimeout(6000);
let finalTime = newTime;
do {
await page.waitForTimeout(2000);
finalTime = Date.parse(
(await request.get("/methods/get/revalidate").then((res) => res.json()))
.time,
);
} while (newTime === finalTime);

expect(newTime).not.toEqual(finalTime);
});

test("should cache a static GET route", async ({ request }) => {
const res = await request.get("/methods/get/static");
expect(res.headers()["cache-control"]).toBe("s-maxage=31536000,");
});

test("should be able to set cookies in route handler", async ({ request }) => {
const postRes = await request.post("/methods/post/cookies", {
form: {
username: "hakuna",
password: "matata",
},
});
expect(postRes.status()).toBe(202);
const postData = await postRes.json();
expect(postData.message).toBe("ok");
const cookies = postRes.headers()["set-cookie"];
expect(cookies).toContain("auth_session=SUPER_SECRET_SESSION_ID_1234");
});

test("should be able to redirect in route handler", async ({ request }) => {
const redirectRes = await request.get("/methods/get/redirect", {
// Disable auto-redirect to check initial response
maxRedirects: 0,
});
expect(redirectRes.status()).toBe(307);
expect(redirectRes.headers().location).toBe("https://nextjs.org/");

// Check if the redirect works
const followedRes = await request.get("/methods/get/redirect");
expect(followedRes.url()).toBe("https://nextjs.org/");
});

test("dynamic segments should work in route handlers", async ({ request }) => {
const res = await request.get("/methods/get/dynamic-segments/this-is-a-slug");
const data = await res.json();
expect(data.slug).toBe("this-is-a-slug");
});

test("query parameters should work in route handlers", async ({ request }) => {
const res = await request.get("/methods/get/query", {
params: {
query: "OpenNext is awesome!",
},
});
const data = await res.json();
expect(data.query).toBe("OpenNext is awesome!");
});
Loading