From 4695152e873996bfa815803b3e37cdc64ffe13ec Mon Sep 17 00:00:00 2001 From: Miguel Angel Simon Sierra Date: Sat, 14 Nov 2026 11:40:13 -0500 Subject: [PATCH 1/2] fix(engine): look a host up again after a failed lookup instead of refusing it for the whole page --- .../pageNetworkPolicy.browser.test.ts | 13 +++++++- .../engine/src/services/pageNetworkPolicy.ts | 32 ++++++++++++------- 2 files changed, 33 insertions(+), 12 deletions(-) diff --git a/packages/engine/src/services/pageNetworkPolicy.browser.test.ts b/packages/engine/src/services/pageNetworkPolicy.browser.test.ts index c996e6e8b6..020dc9882b 100644 --- a/packages/engine/src/services/pageNetworkPolicy.browser.test.ts +++ b/packages/engine/src/services/pageNetworkPolicy.browser.test.ts @@ -15,7 +15,11 @@ const answers: Record = { "meta.hf-test": ["169.254.169.254"], "public.hf-test": ["8.8.8.8"], }; +let flakyLookups = 0; const resolveHost = async (host: string) => { + // Fails its first lookup only, like a passing DNS blip. + if (host === "flaky.hf-test" && flakyLookups++ === 0) throw new Error(`EAI_AGAIN ${host}`); + if (host === "flaky.hf-test") return ["8.8.4.4"]; const found = answers[host]; if (!found) throw new Error(`ENOTFOUND ${host}`); return found; @@ -64,7 +68,7 @@ describe.skipIf(!hasBrowser)("restrictPageNetwork in Chrome", () => { // The render browser runs frames in-process (no site isolation); mirror that. browser = await launchTestChrome(chromePath, [ "--disable-features=IsolateOrigins,site-per-process", - "--host-resolver-rules=MAP private.hf-test 127.0.0.1, MAP public.hf-test 127.0.0.1, MAP meta.hf-test 169.254.169.254", + "--host-resolver-rules=MAP private.hf-test 127.0.0.1, MAP public.hf-test 127.0.0.1, MAP flaky.hf-test 127.0.0.1, MAP meta.hf-test 169.254.169.254", ]); }); afterEach(async () => { @@ -174,6 +178,13 @@ describe.skipIf(!hasBrowser)("restrictPageNetwork in Chrome", () => { expect(otherHits).toEqual(["/public"]); }); + it("asks again after a lookup fails, rather than refusing the host for the rest of the page", async () => { + const restricted = await openRestricted([]); + expect(await fetchText(restricted, `http://flaky.hf-test:${otherPort}/first`)).toBe("refused"); + expect(await fetchText(restricted, `http://flaky.hf-test:${otherPort}/second`)).toBe("secret"); + expect(otherHits).toEqual(["/second"]); + }); + it("refuses a name it cannot resolve", async () => { const refused: string[] = []; const restricted = await openRestricted(refused); diff --git a/packages/engine/src/services/pageNetworkPolicy.ts b/packages/engine/src/services/pageNetworkPolicy.ts index 687030f4cf..97de70a297 100644 --- a/packages/engine/src/services/pageNetworkPolicy.ts +++ b/packages/engine/src/services/pageNetworkPolicy.ts @@ -8,16 +8,20 @@ type ResolveHost = (hostname: string) => Promise; const systemResolveHost: ResolveHost = async (hostname) => (await lookup(hostname, { all: true, verbatim: true })).map(({ address }) => address); -async function isPublicHost(hostname: string, resolveHost: ResolveHost): Promise { +type HostVerdict = "public" | "non-public" | "unresolved"; + +async function hostVerdict(hostname: string, resolveHost: ResolveHost): Promise { const host = hostname.replace(/^\[|\]$/g, ""); - if (isBlockedNetworkHost(host)) return false; - if (isIP(host)) return true; + if (isBlockedNetworkHost(host)) return "non-public"; + if (isIP(host)) return "public"; + let addresses: string[]; try { - const addresses = await resolveHost(host); - return addresses.length > 0 && !addresses.some((a) => isBlockedNetworkHost(a.split("%")[0]!)); + addresses = await resolveHost(host); } catch { - return false; + return "unresolved"; } + if (addresses.length === 0) return "unresolved"; + return addresses.some((a) => isBlockedNetworkHost(a.split("%")[0]!)) ? "non-public" : "public"; } /** @@ -30,7 +34,7 @@ export async function restrictPageNetwork( resolveHost: ResolveHost = systemResolveHost, ): Promise { const own = new URL(allowedOrigin).origin; - const verdicts = new Map>(); + const verdicts = new Map>(); const allows = (url: string): Promise => { let target: URL; try { @@ -41,12 +45,18 @@ export async function restrictPageNetwork( if (target.origin === own || !/^(https?|wss?):$/.test(target.protocol)) { return Promise.resolve(true); } - let verdict = verdicts.get(target.hostname); + const host = target.hostname; + let verdict = verdicts.get(host); if (!verdict) { - verdict = isPublicHost(target.hostname, resolveHost); - verdicts.set(target.hostname, verdict); + const asked = hostVerdict(host, resolveHost); + verdicts.set(host, asked); + // A failed lookup can be a passing blip, so only answers about resolved addresses are kept. + void asked.then((answer) => { + if (answer === "unresolved" && verdicts.get(host) === asked) verdicts.delete(host); + }); + verdict = asked; } - return verdict; + return verdict.then((answer) => answer === "public"); }; const client: CDPSession = await page.createCDPSession(); client.on("Fetch.requestPaused", (event: Protocol.Fetch.RequestPausedEvent) => { From b1d3b1dc9b3ec916fb5f4c0922dd9ccfe2749389 Mon Sep 17 00:00:00 2001 From: Miguel Angel Simon Sierra Date: Sat, 14 Nov 2026 11:55:23 -0500 Subject: [PATCH 2/2] fix(engine): name the failed-lookup reset instead of commenting it --- packages/engine/src/services/pageNetworkPolicy.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/engine/src/services/pageNetworkPolicy.ts b/packages/engine/src/services/pageNetworkPolicy.ts index 97de70a297..63cf4617c9 100644 --- a/packages/engine/src/services/pageNetworkPolicy.ts +++ b/packages/engine/src/services/pageNetworkPolicy.ts @@ -35,6 +35,9 @@ export async function restrictPageNetwork( ): Promise { const own = new URL(allowedOrigin).origin; const verdicts = new Map>(); + const forgetFailedLookup = (host: string, asked: Promise, answer: HostVerdict) => { + if (answer === "unresolved" && verdicts.get(host) === asked) verdicts.delete(host); + }; const allows = (url: string): Promise => { let target: URL; try { @@ -50,10 +53,7 @@ export async function restrictPageNetwork( if (!verdict) { const asked = hostVerdict(host, resolveHost); verdicts.set(host, asked); - // A failed lookup can be a passing blip, so only answers about resolved addresses are kept. - void asked.then((answer) => { - if (answer === "unresolved" && verdicts.get(host) === asked) verdicts.delete(host); - }); + void asked.then((answer) => forgetFailedLookup(host, asked, answer)); verdict = asked; } return verdict.then((answer) => answer === "public");