Skip to content

Add request ID handling #914

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jun 23, 2025
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
5 changes: 5 additions & 0 deletions .changeset/orange-badgers-cross.md
Original file line number Diff line number Diff line change
@@ -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
12 changes: 2 additions & 10 deletions examples/sst/stacks/AppRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`,
Expand Down
7 changes: 6 additions & 1 deletion packages/open-next/src/adapters/edge-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -18,9 +19,13 @@ const defaultHandler = async (
): Promise<InternalResult> => {
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");
Expand Down
10 changes: 10 additions & 0 deletions packages/open-next/src/adapters/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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);
Expand All @@ -74,6 +78,7 @@ const defaultHandler = async (
[INTERNAL_HEADER_RESOLVED_ROUTES]: JSON.stringify(
result.resolvedRoutes,
),
[INTERNAL_EVENT_REQUEST_ID]: requestId,
},
},
isExternalRewrite: result.isExternalRewrite,
Expand All @@ -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",
Expand All @@ -105,6 +114,7 @@ const defaultHandler = async (
}
}

result.headers[INTERNAL_EVENT_REQUEST_ID] = requestId;
debug("Middleware response", result);
return result;
},
Expand Down
8 changes: 8 additions & 0 deletions packages/open-next/src/core/requestHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -40,11 +41,18 @@ export async function openNextHandler(
options?: OpenNextHandlerOptions,
): Promise<InternalResult> {
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");
Expand Down
2 changes: 2 additions & 0 deletions packages/open-next/src/core/routing/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
1 change: 1 addition & 0 deletions packages/open-next/src/core/routingHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion packages/open-next/src/utils/promise.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,17 +105,19 @@ export function runWithOpenNextRequestContext<T>(
{
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<T>,
): Promise<T> {
return globalThis.__openNextAls.run(
{
requestId: Math.random().toString(36),
requestId,
pendingPromiseRunner: new DetachedPromiseRunner(),
isISRRevalidation,
waitUntil,
Expand Down
3 changes: 3 additions & 0 deletions packages/tests-e2e/tests/appRouter/headers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
3 changes: 3 additions & 0 deletions packages/tests-e2e/tests/pagesRouter/header.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
Loading