diff --git a/.changeset/pink-years-teach.md b/.changeset/pink-years-teach.md new file mode 100644 index 000000000..e4c984c0f --- /dev/null +++ b/.changeset/pink-years-teach.md @@ -0,0 +1,8 @@ +--- +"@opennextjs/aws": patch +--- + +fix: Correct external URL detection in isExternal using proper URL parsing + +Replaces substring-based host matching with URL parsing to correctly determine whether a rewritten URL is external. +This fixes an issue where NextResponse.rewrite() would treat certain external URLs as internal when their pathname contained the host as a substring, causing unexpected 404s during middleware rewrites. diff --git a/packages/open-next/src/core/routing/util.ts b/packages/open-next/src/core/routing/util.ts index 149adfded..f29192074 100644 --- a/packages/open-next/src/core/routing/util.ts +++ b/packages/open-next/src/core/routing/util.ts @@ -30,10 +30,18 @@ import { generateMessageGroupId } from "./queue.js"; export function isExternal(url?: string, host?: string) { if (!url) return false; const pattern = /^https?:\/\//; + if (!pattern.test(url)) return false; + if (host) { - return pattern.test(url) && !url.includes(host); + try { + const parsedUrl = new URL(url); + return parsedUrl.host !== host; + } catch { + // If URL parsing fails, fall back to substring check + return !url.includes(host); + } } - return pattern.test(url); + return true; } export function convertFromQueryString(query: string) { diff --git a/packages/tests-unit/tests/core/routing/util.test.ts b/packages/tests-unit/tests/core/routing/util.test.ts index e4ba8dbab..5160e6553 100644 --- a/packages/tests-unit/tests/core/routing/util.test.ts +++ b/packages/tests-unit/tests/core/routing/util.test.ts @@ -87,6 +87,24 @@ describe("isExternal", () => { it("returns false for absolute https url same host", () => { expect(isExternal("https://absolute.com/path", "absolute.com")).toBe(false); }); + + it("returns true for absolute https url different host but same host in path", () => { + expect(isExternal("https://absolute.com/local.com", "local.com")).toBe( + true, + ); + }); + + it("returns false for same host with port", () => { + expect(isExternal("https://localhost:3000/path", "localhost:3000")).toBe( + false, + ); + }); + + it("returns true for different port on same hostname", () => { + expect(isExternal("https://localhost:3001/path", "localhost:3000")).toBe( + true, + ); + }); }); describe("convertFromQueryString", () => {