Skip to content

Commit 3e05bca

Browse files
committed
fix: Ensure middleware rewrite status code is properly propagated in cache interceptor
1 parent fe913bb commit 3e05bca

File tree

7 files changed

+93
-10
lines changed

7 files changed

+93
-10
lines changed

packages/open-next/src/adapters/middleware.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,13 @@ const defaultHandler = async (
125125
}
126126
}
127127

128-
result.headers[INTERNAL_EVENT_REQUEST_ID] = requestId;
128+
if (process.env.OPEN_NEXT_REQUEST_ID_HEADER || globalThis.openNextDebug) {
129+
result.headers[INTERNAL_EVENT_REQUEST_ID] = requestId;
130+
}
131+
132+
// Can safely delete this header here before returning as it is only used for the cache tags internally
133+
delete result.headers["x-next-cache-tags"];
134+
129135
debug("Middleware response", result);
130136
return result;
131137
},

packages/open-next/src/core/requestHandler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export async function openNextHandler(
6363
}
6464
debug("internalEvent", internalEvent);
6565

66-
// These 2 will get overwritten by the routing handler if not using an external middleware
66+
// These 3 will get overwritten by the routing handler if not using an external middleware
6767
const internalHeaders = {
6868
initialPath:
6969
initialHeaders[INTERNAL_HEADER_INITIAL_URL] ?? internalEvent.rawPath,

packages/open-next/src/core/routing/cacheInterceptor.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -132,8 +132,12 @@ async function generateResult(
132132
);
133133
return {
134134
type: "core",
135-
// sometimes other status codes can be cached, like 404. For these cases, we should return the correct status code
136-
statusCode: cachedValue.meta?.status ?? 200,
135+
// Sometimes other status codes can be cached, like 404. For these cases, we should return the correct status code
136+
// Also set the status code to the rewriteStatusCode if defined
137+
// This can happen in handleMiddleware in routingHandler.
138+
// `NextResponse.rewrite(url, { status: xxx})
139+
// The rewrite status code should take precedence over the cached one
140+
statusCode: event.rewriteStatusCode ?? cachedValue.meta?.status ?? 200,
137141
body: toReadableStream(body, false),
138142
isBase64Encoded: false,
139143
headers: {
@@ -287,7 +291,8 @@ export async function cacheInterceptor(
287291

288292
return {
289293
type: "core",
290-
statusCode: cachedData.value.meta?.status ?? 200,
294+
statusCode:
295+
event.rewriteStatusCode ?? cachedData.value.meta?.status ?? 200,
291296
body: toReadableStream(cachedData.value.body, isBinary),
292297
headers: {
293298
...cacheControl,

packages/open-next/src/core/routing/middleware.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,7 @@ export async function handleMiddleware(
198198
cookies: internalEvent.cookies,
199199
remoteAddress: internalEvent.remoteAddress,
200200
isExternalRewrite,
201-
rewriteStatusCode: statusCode,
201+
rewriteStatusCode:
202+
rewriteUrl && !isExternalRewrite ? statusCode : undefined,
202203
} satisfies MiddlewareEvent;
203204
}

packages/open-next/src/types/open-next.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ export type InternalEvent = {
3030
readonly query: Record<string, string | string[]>;
3131
readonly cookies: Record<string, string>;
3232
readonly remoteAddress: string;
33+
// Used for NextResponse.rewrite's status code in middleware
34+
readonly rewriteStatusCode?: number;
3335
} & BaseEventOrResult<"core">;
3436

3537
export type InternalResult = {

packages/tests-e2e/tests/appRouter/middleware.rewrite.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,21 +23,21 @@ test("Middleware Rewrite", async ({ page }) => {
2323
});
2424

2525
test("Middleware Rewrite External Image", async ({ page }) => {
26-
await page.goto("/rewrite-external");
2726
page.on("response", async (response) => {
2827
expect(response.status()).toBe(200);
2928
expect(response.headers()["content-type"]).toBe("image/png");
3029
expect(response.headers()["cache-control"]).toBe("max-age=600");
3130
const bodyBuffer = await response.body();
3231
expect(validateMd5(bodyBuffer, OPENNEXT_PNG_MD5)).toBe(true);
3332
});
33+
await page.goto("/rewrite-external");
3434
});
3535

3636
test("Middleware Rewrite Status Code", async ({ page }) => {
37-
await page.goto("/rewrite-status-code");
38-
const el = page.getByText("Rewritten Destination", { exact: true });
39-
await expect(el).toBeVisible();
4037
page.on("response", async (response) => {
4138
expect(response.status()).toBe(403);
4239
});
40+
await page.goto("/rewrite-status-code");
41+
const el = page.getByText("Rewritten Destination", { exact: true });
42+
await expect(el).toBeVisible();
4343
});

packages/tests-unit/tests/core/routing/cacheInterceptor.test.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ function createEvent(event: PartialEvent): InternalEvent {
4545
query: convertFromQueryString(qs ?? ""),
4646
cookies: event.cookies ?? {},
4747
remoteAddress: event.remoteAddress ?? "::1",
48+
rewriteStatusCode: event.rewriteStatusCode,
4849
};
4950
}
5051

@@ -452,4 +453,72 @@ describe("cacheInterceptor", () => {
452453
}),
453454
);
454455
});
456+
457+
it("should return the rewrite status code when there is active cache", async () => {
458+
const event = createEvent({
459+
url: "/albums",
460+
rewriteStatusCode: 403,
461+
});
462+
incrementalCache.get.mockResolvedValueOnce({
463+
value: {
464+
type: "app",
465+
html: "Hello, world!",
466+
},
467+
});
468+
469+
const result = await cacheInterceptor(event);
470+
expect(result.statusCode).toBe(403);
471+
});
472+
473+
it("should return the rewriteStatusCode if there is a cached status code", async () => {
474+
const event = createEvent({
475+
url: "/albums",
476+
rewriteStatusCode: 203,
477+
});
478+
incrementalCache.get.mockResolvedValueOnce({
479+
value: {
480+
type: "app",
481+
html: "Hello, world!",
482+
meta: {
483+
status: 404,
484+
},
485+
},
486+
});
487+
488+
const result = await cacheInterceptor(event);
489+
expect(result.statusCode).toBe(203);
490+
});
491+
492+
it("should return the cached status code if there is one", async () => {
493+
const event = createEvent({
494+
url: "/albums",
495+
});
496+
incrementalCache.get.mockResolvedValueOnce({
497+
value: {
498+
type: "app",
499+
html: "Hello, world!",
500+
meta: {
501+
status: 405,
502+
},
503+
},
504+
});
505+
506+
const result = await cacheInterceptor(event);
507+
expect(result.statusCode).toBe(405);
508+
});
509+
510+
it("should return 200 if there is no cached status code, nor a rewriteStatusCode", async () => {
511+
const event = createEvent({
512+
url: "/albums",
513+
});
514+
incrementalCache.get.mockResolvedValueOnce({
515+
value: {
516+
type: "app",
517+
html: "Hello, world!",
518+
},
519+
});
520+
521+
const result = await cacheInterceptor(event);
522+
expect(result.statusCode).toBe(200);
523+
});
455524
});

0 commit comments

Comments
 (0)