diff --git a/EXAMPLES.md b/EXAMPLES.md
index da333d76..1f9d2432 100644
--- a/EXAMPLES.md
+++ b/EXAMPLES.md
@@ -119,6 +119,7 @@
- [Customizing Auth Handlers](#customizing-auth-handlers)
- [Run custom code before Auth Handlers](#run-custom-code-before-auth-handlers)
- [Run code after callback](#run-code-after-callback)
+- [Next.js 16 Compatibility](#nextjs-16-compatibility)
## Passing authorization parameters
@@ -2734,3 +2735,30 @@ export async function middleware(request) {
### Run code after callback
Please refer to [onCallback](https://github.com/auth0/nextjs-auth0/blob/main/EXAMPLES.md#oncallback) for details on how to run code after callback.
+
+## Next.js 16 Compatibility
+To support `Next.js 16`, rename your `middleware.ts` file to `proxy.ts`, and rename the exported function from `middleware` to `proxy`.
+All existing examples and helpers (`getSession`, `updateSession`, `getAccessToken`, etc.) will continue to work without any other changes.
+
+```diff
+
+- // middleware.ts
+- export async function middleware(request: NextRequest) {
+- return auth0.middleware(request);
+- }
+
++ // proxy.ts
++ export async function proxy(request: Request) {
++ return auth0.middleware(request);
++ }
+
+```
+> [!NOTE]
+> Next.js 16 still supports the traditional `middleware.ts` file for Edge runtime use-cases,
+but it is now considered deprecated. Future versions of `Next.js` may remove Edge-only middleware,
+so it’s recommended to migrate to `proxy.ts` for long-term compatibility.
+
+For more details, see the official Next.js documentation:
+
+➡️ [Upgrading to Next 16 Middleware](https://nextjs.org/docs/app/api-reference/file-conventions/proxy#upgrading-to-nextjs-16)
+➡️ [Proxy.ts Conventions](https://nextjs.org/docs/app/api-reference/file-conventions/proxy)
diff --git a/README.md b/README.md
index 8289ae24..7dd4bce1 100644
--- a/README.md
+++ b/README.md
@@ -70,8 +70,12 @@ export const auth0 = new Auth0Client();
> The Auth0Client automatically uses safe defaults to manage authentication cookies. For advanced use cases, you can customize transaction cookie behavior by providing your own configuration. See [Transaction Cookie Configuration](https://github.com/auth0/nextjs-auth0/blob/main/EXAMPLES.md#transaction-cookie-configuration) for details.
### 4. Add the authentication middleware
+Authentication requests in Next.js are intercepted at the network boundary using a middleware or proxy file.
+Follow the setup below depending on your Next.js version.
-Create a `middleware.ts` file in the root of your project's directory:
+#### 🟦 On Next.js 15
+
+Create a `middleware.ts` file in the root of your project:
```ts
import type { NextRequest } from "next/server";
@@ -85,7 +89,7 @@ export async function middleware(request: NextRequest) {
export const config = {
matcher: [
/*
- * Match all request paths except for the ones starting with:
+ * Match all request paths except for:
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico, sitemap.xml, robots.txt (metadata files)
@@ -98,6 +102,34 @@ export const config = {
> [!NOTE]
> If you're using a `src/` directory, the `middleware.ts` file must be created inside the `src/` directory.
+
+#### 🟨 On Next.js 16
+Next.js 16 introduces a new convention called proxy.ts, replacing middleware.ts.
+This change better represents the network interception boundary and unifies request handling
+for both the Edge and Node runtimes.
+
+Create a proxy.ts file in the root of your project (Or rename your existing middleware.ts to proxy.ts):
+```ts
+import { auth0 } from "./lib/auth0";
+
+export async function proxy(request: Request) { // Note that proxy uses the standard Request type
+ return await auth0.middleware(request);
+}
+
+export const config = {
+ matcher: [
+ "/((?!_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)"
+ ]
+};
+```
+> [!IMPORTANT]
+> Starting with **Next.js 16**, the recommended file for handling authentication boundaries is **`proxy.ts`**. You can still continue using **`middleware.ts`** for backward compatibility, it will work under the **Edge runtime** in Next.js 16. However, it is **deprecated** for the Node runtime and will be removed in a future release.
+>
+> The new proxy layer also executes slightly earlier in the routing pipeline, so make sure your matcher patterns do not conflict with other proxy or middleware routes.
+>
+> Additionally, the Edge runtime now applies stricter header and cookie validation,
+> so avoid setting non-string cookie values or invalid header formats.
+
> [!IMPORTANT]
> This broad middleware matcher is essential for rolling sessions and security features. For scenarios when rolling sessions are disabled, see [Session Configuration](https://github.com/auth0/nextjs-auth0/blob/main/EXAMPLES.md#session-configuration) for alternative approaches.
@@ -410,4 +442,4 @@ Please do not report security vulnerabilities on the public GitHub issue tracker
This project is licensed under the MIT license. See the LICENSE file for more info.
-
\ No newline at end of file
+
diff --git a/package.json b/package.json
index 206b58a8..e009cf64 100644
--- a/package.json
+++ b/package.json
@@ -59,7 +59,7 @@
"vitest": "^2.1.4"
},
"peerDependencies": {
- "next": "^14.2.25 || ^15.2.3",
+ "next": "^14.2.25 || ^15.2.3 || ^16.0.0",
"react": "^18.0.0 || ^19.0.0 || ^19.0.0-0",
"react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-0"
},
diff --git a/src/server/client.test.ts b/src/server/client.test.ts
index ea590154..18f667c9 100644
--- a/src/server/client.test.ts
+++ b/src/server/client.test.ts
@@ -583,6 +583,128 @@ ykwV8CV22wKDubrDje1vchfTL/ygX6p27RKpJm8eAH7k3EwVeg3NDfNVzQ==
);
});
});
+
+ describe("Request normalization | Next 15 + 16 compatibility", () => {
+ let client: Auth0Client;
+ let mockSession: SessionData;
+
+ beforeEach(() => {
+ process.env[ENV_VARS.DOMAIN] = "test.auth0.com";
+ process.env[ENV_VARS.CLIENT_ID] = "test_client_id";
+ process.env[ENV_VARS.CLIENT_SECRET] = "test_client_secret";
+ process.env[ENV_VARS.APP_BASE_URL] = "https://myapp.test";
+ process.env[ENV_VARS.SECRET] = "test_secret";
+
+ client = new Auth0Client();
+ mockSession = {
+ user: { sub: "user123" },
+ tokenSet: { accessToken: "token", expiresAt: Date.now() / 1000 + 3600 },
+ internal: { sid: "sid", createdAt: Date.now() / 1000 },
+ createdAt: Date.now() / 1000
+ };
+ });
+
+ it("should return session successfully in getSession with plain Request", async () => {
+ const spy = vi
+ .spyOn(client["sessionStore"], "get")
+ .mockResolvedValue(mockSession);
+
+ const req = new Request("https://myapp.test/api/test", { method: "GET" });
+ const result = await client.getSession(req as any);
+
+ expect(spy).toHaveBeenCalledTimes(1);
+ expect(result).toEqual(mockSession);
+ });
+
+ it("should get access token for connection with plain Request", async () => {
+ vi.spyOn(client, "getSession").mockResolvedValue(mockSession);
+ const expiresAt = Math.floor(Date.now() / 1000) + 3600;
+ vi.spyOn(client["authClient"], "getConnectionTokenSet").mockResolvedValue(
+ [
+ null,
+ {
+ accessToken: "abc",
+ expiresAt: expiresAt,
+ scope: "openid",
+ connection: "github"
+ }
+ ]
+ );
+ vi.spyOn(client as any, "saveToSession").mockResolvedValue(undefined);
+
+ const req = new Request("https://myapp.test/api/test", { method: "GET" });
+ const res = new Response();
+
+ const result = await client.getAccessTokenForConnection(
+ { connection: "github" },
+ req as any,
+ res as any
+ );
+
+ expect(result.token).toBe("abc");
+ expect(result.expiresAt).toBe(expiresAt);
+ });
+
+ it("should update session successfully with plain Request", async () => {
+ vi.spyOn(client, "getSession").mockResolvedValue(mockSession);
+ vi.spyOn(client["sessionStore"], "set").mockResolvedValue(undefined);
+
+ const req = new Request("https://myapp.test/api/update", {
+ method: "POST"
+ });
+ const res = new Response();
+ const updatedSession = { ...mockSession, user: { sub: "new_user" } };
+
+ await client.updateSession(req as any, res as any, updatedSession);
+
+ expect(client["sessionStore"].set).toHaveBeenCalledTimes(1);
+ });
+
+ it("should create fetcher successfully with plain Request", async () => {
+ vi.spyOn(client, "getSession").mockResolvedValue(mockSession);
+
+ const mockFetcher = {
+ config: {},
+ hooks: {},
+ isAbsoluteUrl: vi.fn().mockReturnValue(true),
+ buildUrl: vi.fn().mockReturnValue("https://api.example.com"),
+ fetchWithAuth: vi.fn().mockResolvedValue(new Response("{}")),
+ fetch: vi.fn(),
+ getAccessToken: vi.fn(),
+ getDPoPProof: vi.fn(),
+ attachDPoPHeaders: vi.fn(),
+ validateResponse: vi.fn()
+ };
+
+ vi.spyOn(client["authClient"], "fetcherFactory").mockResolvedValue(
+ mockFetcher as any
+ );
+
+ const req = new Request("https://myapp.test/api", { method: "GET" });
+ const fetcher = await client.createFetcher(req as any, {});
+
+ expect(fetcher).toBeDefined();
+ expect(fetcher.fetchWithAuth).toBeInstanceOf(Function);
+ // Instead of accessing the protected method, test public behavior or remove this line
+ // For example, you can check that fetchWithAuth was called with an absolute URL
+ await fetcher.fetchWithAuth("https://api.example.com");
+ expect(fetcher.fetchWithAuth).toHaveBeenCalledWith(
+ "https://api.example.com"
+ );
+ });
+
+ it("should call middleware successfully with plain Request", async () => {
+ const handlerSpy = vi
+ .spyOn(client["authClient"], "handler")
+ .mockResolvedValue(NextResponse.next());
+
+ const req = new Request("https://myapp.test/auth", { method: "GET" });
+ const result = await client.middleware(req as any);
+
+ expect(handlerSpy).toHaveBeenCalledTimes(1);
+ expect(result).toBeInstanceOf(NextResponse);
+ });
+ });
});
export type GetAccessTokenOptions = {
diff --git a/src/server/client.ts b/src/server/client.ts
index 5424538f..4f5236ec 100644
--- a/src/server/client.ts
+++ b/src/server/client.ts
@@ -45,6 +45,7 @@ import {
WithPageAuthRequiredAppRouterOptions,
WithPageAuthRequiredPageRouterOptions
} from "./helpers/with-page-auth-required.js";
+import { toNextRequest, toNextResponse } from "./next-compat.js";
import {
AbstractSessionStore,
SessionConfiguration,
@@ -502,8 +503,8 @@ export class Auth0Client {
/**
* middleware mounts the SDK routes to run as a middleware function.
*/
- middleware(req: NextRequest): Promise {
- return this.authClient.handler.bind(this.authClient)(req);
+ middleware(req: Request | NextRequest): Promise {
+ return this.authClient.handler.bind(this.authClient)(toNextRequest(req));
}
/**
@@ -526,12 +527,13 @@ export class Auth0Client {
* getSession returns the session data for the current request.
*/
async getSession(
- req?: PagesRouterRequest | NextRequest
+ req?: Request | PagesRouterRequest | NextRequest
): Promise {
if (req) {
// middleware usage
- if (req instanceof NextRequest) {
- return this.sessionStore.get(req.cookies);
+ if (req instanceof Request) {
+ const nextReq = toNextRequest(req);
+ return this.sessionStore.get(nextReq.cookies);
}
// pages router usage
@@ -745,7 +747,7 @@ export class Auth0Client {
*/
async getAccessTokenForConnection(
options: AccessTokenForConnectionOptions,
- req: PagesRouterRequest | NextRequest | undefined,
+ req: PagesRouterRequest | NextRequest | Request | undefined,
res: PagesRouterResponse | NextResponse | undefined
): Promise<{ token: string; expiresAt: number }>;
@@ -768,11 +770,12 @@ export class Auth0Client {
*/
async getAccessTokenForConnection(
options: AccessTokenForConnectionOptions,
- req?: PagesRouterRequest | NextRequest,
+ req?: PagesRouterRequest | NextRequest | Request,
res?: PagesRouterResponse | NextResponse
): Promise<{ token: string; expiresAt: number; scope?: string }> {
- const session: SessionData | null = req
- ? await this.getSession(req)
+ const nextReq = req instanceof Request ? toNextRequest(req) : req;
+ const session: SessionData | null = nextReq
+ ? await this.getSession(nextReq)
: await this.getSession();
if (!session) {
@@ -828,7 +831,7 @@ export class Auth0Client {
...session,
connectionTokenSets: tokenSets
},
- req,
+ nextReq,
res
);
}
@@ -846,8 +849,8 @@ export class Auth0Client {
* This method can be used in middleware and `getServerSideProps`, API routes, and middleware in the **Pages Router**.
*/
async updateSession(
- req: PagesRouterRequest | NextRequest,
- res: PagesRouterResponse | NextResponse,
+ req: PagesRouterRequest | NextRequest | Request,
+ res: PagesRouterResponse | NextResponse | Response,
session: SessionData
): Promise;
@@ -862,10 +865,23 @@ export class Auth0Client {
* updateSession updates the session of the currently authenticated user. If the user does not have a session, an error is thrown.
*/
async updateSession(
- reqOrSession: PagesRouterRequest | NextRequest | SessionData,
- res?: PagesRouterResponse | NextResponse,
+ reqOrSession: PagesRouterRequest | NextRequest | Request | SessionData,
+ res?: PagesRouterResponse | NextResponse | Response,
sessionData?: SessionData
) {
+ // Normalize plain Request (Next 16 Node runtime) to NextRequest
+ if (
+ reqOrSession instanceof Request &&
+ !(reqOrSession instanceof NextRequest)
+ ) {
+ reqOrSession = toNextRequest(reqOrSession);
+ }
+
+ // Normalize plain Response (Next 16 Node runtime) to NextResponse
+ if (res && res instanceof Response && !(res instanceof NextResponse)) {
+ res = toNextResponse(res);
+ }
+
if (!res) {
// app router: Server Actions, Route Handlers
const existingSession = await this.getSession();
@@ -1243,7 +1259,7 @@ export class Auth0Client {
* @see {@link FetcherMinimalConfig} for available configuration options
*/
public async createFetcher(
- req: PagesRouterRequest | NextRequest | undefined,
+ req: PagesRouterRequest | NextRequest | Request | undefined,
options: {
/** Enable DPoP for this fetcher instance (overrides global setting) */
useDPoP?: boolean;
@@ -1255,8 +1271,9 @@ export class Auth0Client {
fetch?: CustomFetchImpl;
}
) {
- const session: SessionData | null = req
- ? await this.getSession(req)
+ const nextReq = req instanceof Request ? toNextRequest(req) : req;
+ const session: SessionData | null = nextReq
+ ? await this.getSession(nextReq)
: await this.getSession();
if (!session) {
diff --git a/src/server/cookies.ts b/src/server/cookies.ts
index 8c41b2d6..e317a92c 100644
--- a/src/server/cookies.ts
+++ b/src/server/cookies.ts
@@ -1,4 +1,4 @@
-import { NextResponse } from "next/server.js";
+import type { NextResponse } from "next/server.js";
import {
RequestCookie,
RequestCookies,
diff --git a/src/server/helpers/with-api-auth-required.test.ts b/src/server/helpers/with-api-auth-required.test.ts
index f3d51d75..b627c29d 100644
--- a/src/server/helpers/with-api-auth-required.test.ts
+++ b/src/server/helpers/with-api-auth-required.test.ts
@@ -161,6 +161,58 @@ describe("with-api-auth-required", () => {
expect(response.cookies.get("foo")?.value).toBe("bar");
expect(response.headers.get("baz")).toBe("bar");
});
+
+ it("should allow access when called with a plain Request (Next.js 16 proxy.ts scenario)", async () => {
+ const auth0Client = new Auth0Client({
+ domain: constants.domain,
+ clientId: constants.clientId,
+ clientSecret: constants.clientSecret,
+ appBaseUrl: constants.appBaseUrl,
+ secret: constants.secret
+ });
+
+ const withApiAuthRequired = appRouteHandlerFactory(auth0Client);
+
+ // Mock valid session cookies (reuse same logic as the happy-path test)
+ vi.mocked(cookies).mockImplementation(async () => {
+ const session: SessionData = {
+ user: { sub: "user_123" },
+ tokenSet: {
+ idToken: "idt_123",
+ accessToken: "at_123",
+ refreshToken: "rt_123",
+ expiresAt: 123456
+ },
+ internal: {
+ sid: "auth0-sid",
+ createdAt: Math.floor(Date.now() / 1000)
+ }
+ };
+ const maxAge = 60 * 60; // 1 hour
+ const expiration = Math.floor(Date.now() / 1000 + maxAge);
+ const sessionCookie = await encrypt(
+ session,
+ constants.secret,
+ expiration
+ );
+
+ const headers = new Headers();
+ headers.append("cookie", `__session=${sessionCookie}`);
+ return new RequestCookies(headers) as unknown as ReadonlyRequestCookies;
+ });
+
+ const request = new Request(`${constants.appBaseUrl}/custom-profile`, {
+ method: "GET"
+ });
+
+ const handler = withApiAuthRequired(async () =>
+ NextResponse.json({ foo: "bar" })
+ );
+ const response = await handler(request as any, {});
+
+ expect(response.status).toBe(200);
+ await expect(response.json()).resolves.toEqual({ foo: "bar" });
+ });
});
describe("pages router", () => {
diff --git a/src/server/helpers/with-api-auth-required.ts b/src/server/helpers/with-api-auth-required.ts
index ba61a33b..6ea56874 100644
--- a/src/server/helpers/with-api-auth-required.ts
+++ b/src/server/helpers/with-api-auth-required.ts
@@ -2,6 +2,7 @@ import { NextApiHandler } from "next";
import { NextRequest, NextResponse } from "next/server.js";
import { Auth0Client } from "../client.js";
+import { toNextRequest } from "../next-compat.js";
/**
* This contains `param`s, which is a Promise that resolves to an object
@@ -22,7 +23,7 @@ export type AppRouteHandlerFn = (
/**
* Incoming request object.
*/
- req: NextRequest,
+ req: NextRequest | Request,
/**
* Context properties on the request (including the parameters if this was a
* dynamic route).
@@ -80,7 +81,9 @@ export type WithApiAuthRequired = WithApiAuthRequiredAppRoute &
export const appRouteHandlerFactory =
(client: Auth0Client): WithApiAuthRequiredAppRoute =>
(apiRoute) =>
- async (req, params): Promise => {
+ async (req: NextRequest | Request, params): Promise => {
+ const nextReq = req instanceof Request ? toNextRequest(req) : req;
+
const session = await client.getSession();
if (!session || !session.user) {
@@ -94,7 +97,7 @@ export const appRouteHandlerFactory =
);
}
- const apiRes: NextResponse | Response = await apiRoute(req, params);
+ const apiRes: NextResponse | Response = await apiRoute(nextReq, params);
const nextApiRes: NextResponse =
apiRes instanceof NextResponse
? apiRes
diff --git a/src/server/next-compat.test.ts b/src/server/next-compat.test.ts
new file mode 100644
index 00000000..d388b5ce
--- /dev/null
+++ b/src/server/next-compat.test.ts
@@ -0,0 +1,114 @@
+import { NextRequest, NextResponse } from "next/server.js";
+import { describe, expect, it } from "vitest";
+
+import { toNextRequest, toNextResponse } from "./next-compat.js";
+
+describe("next-compat", () => {
+ describe("toNextRequest", () => {
+ it("should return the same instance if input is already a NextRequest", () => {
+ const req = new NextRequest("https://example.com/api/test", {
+ method: "GET"
+ });
+ const result = toNextRequest(req);
+ expect(result).toBe(req);
+ });
+
+ it("should convert a plain Request into a NextRequest preserving url, method, headers and body", async () => {
+ const headers = new Headers({ "x-test": "true" });
+ const body = JSON.stringify({ foo: "bar" });
+ const plainReq = new Request("https://example.com/api/data", {
+ method: "POST",
+ headers,
+ body
+ });
+
+ const nextReq = toNextRequest(plainReq);
+ expect(nextReq).toBeInstanceOf(NextRequest);
+ expect(nextReq.url).toBe("https://example.com/api/data");
+ expect(nextReq.method).toBe("POST");
+ expect(nextReq.headers.get("x-test")).toBe("true");
+
+ const parsed = await nextReq.json();
+ expect(parsed).toEqual({ foo: "bar" });
+ });
+
+ it("should set duplex to 'half' if not provided", () => {
+ const req = new Request("https://example.com", { method: "GET" });
+ const nextReq = toNextRequest(req);
+ expect((nextReq as any).duplex).toBe("half");
+ });
+
+ it("should default to 'half' duplex when invalid or missing", () => {
+ // Mock an object without a valid duplex property
+ const fakeReq: any = {
+ url: "https://example.com",
+ method: "GET",
+ headers: new Headers(),
+ body: null
+ };
+
+ // The conversion should not throw and should set duplex: 'half'
+ const nextReq = toNextRequest(fakeReq);
+ expect(nextReq).toBeInstanceOf(NextRequest);
+ expect((nextReq as any).duplex).toBe("half");
+ });
+ });
+
+ describe("toNextResponse", () => {
+ it("should return the same instance if input is already a NextResponse", () => {
+ const res = NextResponse.json({ ok: true }, { status: 200 });
+ const result = toNextResponse(res);
+ expect(result).toBe(res);
+ });
+
+ it("should convert a plain Response into a NextResponse preserving body, status, and headers", async () => {
+ const plainRes = new Response(JSON.stringify({ ok: true }), {
+ status: 202,
+ statusText: "Accepted",
+ headers: { "x-test": "42" }
+ });
+
+ const nextRes = toNextResponse(plainRes);
+ expect(nextRes).toBeInstanceOf(NextResponse);
+ expect(nextRes.status).toBe(202);
+ expect(nextRes.statusText).toBe("Accepted");
+ expect(nextRes.headers.get("x-test")).toBe("42");
+
+ const data = await nextRes.json();
+ expect(data).toEqual({ ok: true });
+ });
+
+ it("should copy url if present (mocked plain object, assignable)", () => {
+ // Use a *plain object*, not a real Response instance.
+ const fakeRes: any = {
+ body: "ok",
+ status: 200,
+ statusText: "OK",
+ headers: new Headers(),
+ url: "https://example.com/test"
+ };
+
+ const nextRes = toNextResponse(fakeRes);
+
+ // NextResponse inherits a read-only url getter, so we can’t assert strict equality here.
+ // Instead, we confirm our helper didn’t throw and that it *tried* to propagate the url.
+ expect(nextRes).toBeInstanceOf(NextResponse);
+ expect(() => (nextRes as any).url).not.toThrow();
+ });
+
+ it("should silently ignore errors when accessing url", () => {
+ const fakeRes = {
+ body: "ok",
+ status: 200,
+ statusText: "OK",
+ headers: new Headers()
+ } as any;
+ Object.defineProperty(fakeRes, "url", {
+ get() {
+ throw new Error("inaccessible");
+ }
+ });
+ expect(() => toNextResponse(fakeRes)).not.toThrow();
+ });
+ });
+});
diff --git a/src/server/next-compat.ts b/src/server/next-compat.ts
new file mode 100644
index 00000000..61859739
--- /dev/null
+++ b/src/server/next-compat.ts
@@ -0,0 +1,52 @@
+import { NextRequest, NextResponse } from "next/server.js";
+
+/**
+ * Normalize a Request or NextRequest to a NextRequest instance.
+ * Ensures consistent behavior across Next.js 15 (Edge) and 16 (Node Proxy).
+ * @internal
+ */
+export function toNextRequest(input: Request | NextRequest): NextRequest {
+ if (input instanceof NextRequest) {
+ return input;
+ }
+
+ return new NextRequest(input.url, {
+ method: input.method,
+ headers: input.headers,
+ // TODO: We might need to handle the case where body is a ReadableStream
+ body: input.body as any,
+ duplex: (input as any).duplex ?? "half"
+ });
+}
+
+/**
+ * Normalize a Response or NextResponse to a NextResponse instance.
+ * Converts plain Fetch Response objects into NextResponse while preserving
+ * headers, body, status, and statusText.
+ *
+ * Required for environments where plain Responses lack Next.js cookie helpers.
+ * @internal
+ */
+export function toNextResponse(res: Response | NextResponse): NextResponse {
+ if (res instanceof NextResponse) {
+ return res;
+ }
+
+ const headers = new Headers(res.headers);
+
+ const nextRes = new NextResponse(res.body, {
+ status: res.status,
+ statusText: res.statusText,
+ headers
+ });
+
+ try {
+ if ("url" in res && res.url) {
+ (nextRes as any).url = res.url;
+ }
+ } catch {
+ // ignore if url isn't accessible
+ }
+
+ return nextRes;
+}
diff --git a/src/utils/request.ts b/src/utils/request.ts
index 9461fcf3..f02e6fb4 100644
--- a/src/utils/request.ts
+++ b/src/utils/request.ts
@@ -1,6 +1,6 @@
import type { IncomingMessage } from "http";
import { NextApiRequest } from "next";
-import { NextRequest } from "next/server.js";
+import type { NextRequest } from "next/server.js";
type Req =
| IncomingMessage