fix: NextJS does not support non-ascii in NextResponse headers (#25148)
* fix: NextJS does not support non-ascii in NextResponse headers * Fixed type error
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
// Import mocked functions
|
||||
import { get as edgeConfigGet } from "@vercel/edge-config";
|
||||
import { NextRequest } from "next/server";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import type { Mock } from "vitest";
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
|
||||
@@ -405,6 +405,56 @@ describe("Middleware Integration Tests", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Header sanitization", () => {
|
||||
it("sanitizes non-ASCII request header values to prevent Vercel Runtime Malformed Response Header", async () => {
|
||||
const spy = vi.spyOn(NextResponse, "next");
|
||||
|
||||
const req = createTestRequest({
|
||||
url: `${WEBAPP_URL}/team/test`,
|
||||
// Next.js will translate request overrides into x-middleware-request-* headers internally
|
||||
headers: {
|
||||
"cf-region": "São Paulo", // contains non-ASCII "ã"
|
||||
},
|
||||
});
|
||||
|
||||
const res = await callMiddleware(req);
|
||||
expectStatus(res, 200);
|
||||
|
||||
// Assert that middleware forwarded sanitized ASCII-only value
|
||||
const initArg = (spy as unknown as Mock).mock.calls.at(-1)?.[0] as {
|
||||
request?: { headers?: Headers };
|
||||
};
|
||||
expect(initArg?.request?.headers).toBeDefined();
|
||||
|
||||
const forwarded = initArg?.request?.headers as Headers;
|
||||
expect(forwarded.get("cf-region")).toBe("Sao Paulo");
|
||||
|
||||
spy.mockRestore();
|
||||
});
|
||||
|
||||
it("strips non-ASCII bytes in mojibake values (e.g., 'São Paulo' -> 'So Paulo')", async () => {
|
||||
const spy = vi.spyOn(NextResponse, "next");
|
||||
|
||||
const req = createTestRequest({
|
||||
url: `${WEBAPP_URL}/team/test`,
|
||||
headers: {
|
||||
"cf-region": "São Paulo", // mojibake for "São Paulo"; includes non-ASCII bytes
|
||||
},
|
||||
});
|
||||
|
||||
const res = await callMiddleware(req);
|
||||
expectStatus(res, 200);
|
||||
|
||||
const initArg = (spy as unknown as Mock).mock.calls.at(-1)?.[0] as {
|
||||
request?: { headers?: Headers };
|
||||
};
|
||||
const forwarded = initArg?.request?.headers as Headers;
|
||||
expect(forwarded.get("cf-region")).toBe("So Paulo");
|
||||
|
||||
spy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Multiple Features", () => {
|
||||
|
||||
it("should handle embed route with routing forms rewrite", async () => {
|
||||
|
||||
+43
-1
@@ -85,6 +85,48 @@ export function checkPostMethod(req: NextRequest) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Vercel/Edge rejects non‑ASCII header values (see: https://github.com/vercel/next.js/issues/85631)
|
||||
const isAscii = (s: string) => {
|
||||
for (let i = 0; i < s.length; i++) if (s.charCodeAt(i) > 0x7f) return false;
|
||||
return true;
|
||||
};
|
||||
const stripNonAscii = (s: string) => {
|
||||
let out = "";
|
||||
for (let i = 0; i < s.length; i++) if (s.charCodeAt(i) <= 0x7f) out += s[i];
|
||||
return out;
|
||||
};
|
||||
const sanitizeRequestHeaders = (headers: Iterable<[string, string]>): Headers => {
|
||||
const out = new Headers();
|
||||
for (const [name, raw] of Array.from(headers)) {
|
||||
if (!isAscii(name)) continue;
|
||||
let value = raw;
|
||||
if (!isAscii(value)) {
|
||||
// Heuristic: if the string contains common mojibake markers (Ã: 0xC3, Â: 0xC2),
|
||||
// prefer a simple strip (avoids introducing spurious ASCII letters like 'A').
|
||||
let hasMojibakeMarker = false;
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
const code = value.charCodeAt(i);
|
||||
if (code === 0xc3 || code === 0xc2) {
|
||||
hasMojibakeMarker = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasMojibakeMarker) {
|
||||
value = stripNonAscii(value);
|
||||
} else {
|
||||
try {
|
||||
value = stripNonAscii(value.normalize("NFKD"));
|
||||
} catch {
|
||||
value = stripNonAscii(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (value) out.set(name, value);
|
||||
}
|
||||
return out;
|
||||
};
|
||||
|
||||
const isPagePathRequest = (url: URL) => {
|
||||
const isNonPagePathPrefix = /^\/(?:_next|api)\//;
|
||||
const isFile = /\..*$/;
|
||||
@@ -145,7 +187,7 @@ const middleware = async (req: NextRequest): Promise<NextResponse<unknown>> => {
|
||||
|
||||
const res = NextResponse.next({
|
||||
request: {
|
||||
headers: requestHeaders,
|
||||
headers: sanitizeRequestHeaders(requestHeaders),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user