diff --git a/.changeset/orange-badgers-cross.md b/.changeset/orange-badgers-cross.md new file mode 100644 index 000000000..f855bb5cb --- /dev/null +++ b/.changeset/orange-badgers-cross.md @@ -0,0 +1,5 @@ +--- +"@opennextjs/aws": patch +--- + +add the OPEN_NEXT_REQUEST_ID_HEADER env variable that allow to always have the request id header diff --git a/examples/sst/stacks/AppRouter.ts b/examples/sst/stacks/AppRouter.ts index b34c49d9e..64ecc95e2 100644 --- a/examples/sst/stacks/AppRouter.ts +++ b/examples/sst/stacks/AppRouter.ts @@ -6,18 +6,10 @@ export function AppRouter({ stack }) { path: "../app-router", environment: { OPEN_NEXT_FORCE_NON_EMPTY_RESPONSE: "true", + // We want to always add the request ID header + OPEN_NEXT_REQUEST_ID_HEADER: "true", }, }); - // const site = new NextjsSite(stack, "approuter", { - // path: "../app-router", - // buildCommand: "npm run openbuild", - // bind: [], - // environment: {}, - // timeout: "20 seconds", - // experimental: { - // streaming: true, - // }, - // }); stack.addOutputs({ url: `https://${site.distribution.domainName}`, diff --git a/packages/open-next/src/adapters/edge-adapter.ts b/packages/open-next/src/adapters/edge-adapter.ts index 3310dec20..fa16ccad1 100644 --- a/packages/open-next/src/adapters/edge-adapter.ts +++ b/packages/open-next/src/adapters/edge-adapter.ts @@ -9,6 +9,7 @@ import type { OpenNextHandlerOptions } from "types/overrides"; import { NextConfig } from "../adapters/config"; import { createGenericHandler } from "../core/createGenericHandler"; import { convertBodyToReadableStream } from "../core/routing/util"; +import { INTERNAL_EVENT_REQUEST_ID } from "../core/routingHandler"; globalThis.__openNextAls = new AsyncLocalStorage(); @@ -18,9 +19,13 @@ const defaultHandler = async ( ): Promise => { globalThis.isEdgeRuntime = true; + const requestId = globalThis.openNextConfig.middleware?.external + ? internalEvent.headers[INTERNAL_EVENT_REQUEST_ID] + : Math.random().toString(36); + // We run everything in the async local storage context so that it is available in edge runtime functions return runWithOpenNextRequestContext( - { isISRRevalidation: false, waitUntil: options?.waitUntil }, + { isISRRevalidation: false, waitUntil: options?.waitUntil, requestId }, async () => { // @ts-expect-error - This is bundled const handler = await import("./middleware.mjs"); diff --git a/packages/open-next/src/adapters/middleware.ts b/packages/open-next/src/adapters/middleware.ts index 21bd6fb8d..d6620dddd 100644 --- a/packages/open-next/src/adapters/middleware.ts +++ b/packages/open-next/src/adapters/middleware.ts @@ -17,6 +17,7 @@ import { } from "../core/resolve"; import { constructNextUrl } from "../core/routing/util"; import routingHandler, { + INTERNAL_EVENT_REQUEST_ID, INTERNAL_HEADER_INITIAL_URL, INTERNAL_HEADER_RESOLVED_ROUTES, } from "../core/routingHandler"; @@ -50,11 +51,14 @@ const defaultHandler = async ( ); //#endOverride + const requestId = Math.random().toString(36); + // We run everything in the async local storage context so that it is available in the external middleware return runWithOpenNextRequestContext( { isISRRevalidation: internalEvent.headers["x-isr"] === "1", waitUntil: options?.waitUntil, + requestId, }, async () => { const result = await routingHandler(internalEvent); @@ -74,6 +78,7 @@ const defaultHandler = async ( [INTERNAL_HEADER_RESOLVED_ROUTES]: JSON.stringify( result.resolvedRoutes, ), + [INTERNAL_EVENT_REQUEST_ID]: requestId, }, }, isExternalRewrite: result.isExternalRewrite, @@ -91,6 +96,10 @@ const defaultHandler = async ( type: "middleware", internalEvent: { ...result.internalEvent, + headers: { + ...result.internalEvent.headers, + [INTERNAL_EVENT_REQUEST_ID]: requestId, + }, rawPath: "/500", url: constructNextUrl(result.internalEvent.url, "/500"), method: "GET", @@ -105,6 +114,7 @@ const defaultHandler = async ( } } + result.headers[INTERNAL_EVENT_REQUEST_ID] = requestId; debug("Middleware response", result); return result; }, diff --git a/packages/open-next/src/core/requestHandler.ts b/packages/open-next/src/core/requestHandler.ts index 7296aba19..ba4060a9e 100644 --- a/packages/open-next/src/core/requestHandler.ts +++ b/packages/open-next/src/core/requestHandler.ts @@ -21,6 +21,7 @@ import { createServerResponse, } from "./routing/util"; import routingHandler, { + INTERNAL_EVENT_REQUEST_ID, INTERNAL_HEADER_INITIAL_URL, INTERNAL_HEADER_RESOLVED_ROUTES, MIDDLEWARE_HEADER_PREFIX, @@ -40,11 +41,18 @@ export async function openNextHandler( options?: OpenNextHandlerOptions, ): Promise { const initialHeaders = internalEvent.headers; + // We only use the requestId header if we are using an external middleware + // This is to ensure that no one can spoof the requestId + // When using an external middleware, we always assume that headers cannot be spoofed + const requestId = globalThis.openNextConfig.middleware?.external + ? internalEvent.headers[INTERNAL_EVENT_REQUEST_ID] + : Math.random().toString(36); // We run everything in the async local storage context so that it is available in the middleware as well as in NextServer return runWithOpenNextRequestContext( { isISRRevalidation: initialHeaders["x-isr"] === "1", waitUntil: options?.waitUntil, + requestId, }, async () => { await globalThis.__next_route_preloader("waitUntil"); diff --git a/packages/open-next/src/core/routing/util.ts b/packages/open-next/src/core/routing/util.ts index 70ae53027..ad62d8f1f 100644 --- a/packages/open-next/src/core/routing/util.ts +++ b/packages/open-next/src/core/routing/util.ts @@ -281,6 +281,8 @@ export function addOpenNextHeader(headers: OutgoingHttpHeaders) { } if (globalThis.openNextDebug) { headers["X-OpenNext-Version"] = globalThis.openNextVersion; + } + if (process.env.OPEN_NEXT_REQUEST_ID_HEADER || globalThis.openNextDebug) { headers["X-OpenNext-RequestId"] = globalThis.__openNextAls.getStore()?.requestId; } diff --git a/packages/open-next/src/core/routingHandler.ts b/packages/open-next/src/core/routingHandler.ts index 575e81f16..5b5665c94 100644 --- a/packages/open-next/src/core/routingHandler.ts +++ b/packages/open-next/src/core/routingHandler.ts @@ -35,6 +35,7 @@ export const INTERNAL_HEADER_PREFIX = "x-opennext-"; export const INTERNAL_HEADER_INITIAL_URL = `${INTERNAL_HEADER_PREFIX}initial-url`; export const INTERNAL_HEADER_LOCALE = `${INTERNAL_HEADER_PREFIX}locale`; export const INTERNAL_HEADER_RESOLVED_ROUTES = `${INTERNAL_HEADER_PREFIX}resolved-routes`; +export const INTERNAL_EVENT_REQUEST_ID = `${INTERNAL_HEADER_PREFIX}request-id`; // Geolocation headers starting from Nextjs 15 // See https://github.com/vercel/vercel/blob/7714b1c/packages/functions/src/headers.ts diff --git a/packages/open-next/src/utils/promise.ts b/packages/open-next/src/utils/promise.ts index 9fde876cb..9e687aa0f 100644 --- a/packages/open-next/src/utils/promise.ts +++ b/packages/open-next/src/utils/promise.ts @@ -105,17 +105,19 @@ export function runWithOpenNextRequestContext( { isISRRevalidation, waitUntil, + requestId = Math.random().toString(36), }: { // Whether we are in ISR revalidation isISRRevalidation: boolean; // Extends the liftetime of the runtime after the response is returned. waitUntil?: WaitUntil; + requestId?: string; }, fn: () => Promise, ): Promise { return globalThis.__openNextAls.run( { - requestId: Math.random().toString(36), + requestId, pendingPromiseRunner: new DetachedPromiseRunner(), isISRRevalidation, waitUntil, diff --git a/packages/tests-e2e/tests/appRouter/headers.test.ts b/packages/tests-e2e/tests/appRouter/headers.test.ts index f992c61fe..348811a49 100644 --- a/packages/tests-e2e/tests/appRouter/headers.test.ts +++ b/packages/tests-e2e/tests/appRouter/headers.test.ts @@ -24,4 +24,7 @@ test("Headers", async ({ page }) => { // Both these headers should not be present cause poweredByHeader is false in appRouter expect(headers["x-powered-by"]).toBeFalsy(); expect(headers["x-opennext"]).toBeFalsy(); + + // Request ID header should be set + expect(headers["x-opennext-requestid"]).not.toBeFalsy(); }); diff --git a/packages/tests-e2e/tests/pagesRouter/header.test.ts b/packages/tests-e2e/tests/pagesRouter/header.test.ts index e528bc194..92a7891b9 100644 --- a/packages/tests-e2e/tests/pagesRouter/header.test.ts +++ b/packages/tests-e2e/tests/pagesRouter/header.test.ts @@ -11,4 +11,7 @@ test("should test if poweredByHeader adds the correct headers ", async ({ // Both these headers should be present cause poweredByHeader is true in pagesRouter expect(headers?.["x-powered-by"]).toBe("Next.js"); expect(headers?.["x-opennext"]).toBe("1"); + + // Request ID header should not be set + expect(headers?.["x-opennext-requestid"]).toBeUndefined(); });