fix(cli): capture names the network's proxy when it refuses the connection to the site (#6706)

* fix(cli): capture names the network's proxy when it refuses the connection to the site

* fix(cli): capture words a refused tunnel and an unreachable proxy apart, and echoes no raw address

* fix(cli): shorten the proxy failure comment
This commit is contained in:
Miguel Ángel
2026-10-29 04:08:34 +00:00
committed by GitHub
parent 6c17659337
commit f0c5db6ea2
2 changed files with 85 additions and 5 deletions
@@ -3,6 +3,7 @@ import { afterEach, describe, expect, it, vi } from "vitest";
import { TimeoutError, type Page } from "puppeteer-core";
import {
gotoWhenSent,
explainProxyRefusal,
isNavigationTimeoutError,
navigateForCapture,
networkIdleAttemptTimeoutMs,
@@ -465,3 +466,57 @@ describe("settleAcrossNavigation", () => {
expect(waitForNetworkIdle).not.toHaveBeenCalled();
});
});
describe("a proxy that was in the way", () => {
const failWith = async (message: string, url = "https://shop.example.com/") => {
const goto = vi.fn().mockRejectedValue(new Error(message));
return (await navigateForCapture({ goto }, url, 120_000).then(
() => undefined,
(err: unknown) => err,
)) as Error;
};
it("that refused the tunnel to the site is named, with the code kept", async () => {
const failure = await failWith(
"net::ERR_TUNNEL_CONNECTION_FAILED at https://shop.example.com/x?q=1",
);
expect(failure.message).toBe(
"Your network's proxy refused the connection to shop.example.com. Ask whoever runs it to allow shop.example.com. (net::ERR_TUNNEL_CONNECTION_FAILED)",
);
expect((failure.cause as Error).message).toContain("/x?q=1");
});
it("that could not be reached itself is not asked to allow the site", async () => {
const failure = await failWith("net::ERR_PROXY_CONNECTION_FAILED at https://shop.example.com/");
expect(failure.message).toBe(
"Could not reach your network's proxy. Check the proxy address and that the proxy is running, or ask whoever runs it. (net::ERR_PROXY_CONNECTION_FAILED)",
);
expect(failure.message).not.toContain("allow");
});
it("names no raw address when the site's cannot be read", () => {
const failure = explainProxyRefusal(
new Error("net::ERR_TUNNEL_CONNECTION_FAILED"),
"not a url at all",
) as Error;
expect(failure.message).toContain("connection to the site");
expect(failure.message).not.toContain("not a url");
});
it("is named after a networkidle2 timeout falls back too", async () => {
const goto = vi
.fn()
.mockRejectedValueOnce(new TimeoutError("Navigation timeout of 30000 ms exceeded"))
.mockRejectedValueOnce(new Error("net::ERR_TUNNEL_CONNECTION_FAILED at https://a.example/"));
await expect(navigateForCapture({ goto }, "https://a.example/", 120_000)).rejects.toThrow(
/proxy refused the connection to a\.example/,
);
});
it("leaves every other failure as it was", async () => {
const name = new Error("net::ERR_NAME_NOT_RESOLVED at https://nope.example/");
expect(explainProxyRefusal(name, "https://nope.example/")).toBe(name);
const goto = vi.fn().mockRejectedValue(name);
await expect(navigateForCapture({ goto }, "https://nope.example/", 120_000)).rejects.toBe(name);
});
});
+30 -5
View File
@@ -9,6 +9,30 @@ export { isNavigationTimeoutError };
export const NETWORK_IDLE_ATTEMPT_MS = 30_000;
/** Chromium's words for a proxy in the way: the tunnel to the site was refused or not made, or the proxy itself
* could not be reached. */
const PROXY_FAILURES = {
"net::ERR_TUNNEL_CONNECTION_FAILED": (host: string) =>
`Your network's proxy refused the connection to ${host}. Ask whoever runs it to allow ${host}.`,
"net::ERR_PROXY_CONNECTION_FAILED": () =>
"Could not reach your network's proxy. Check the proxy address and that the proxy is running, or ask whoever runs it.",
} as const;
const PROXY_FAILURE = new RegExp(Object.keys(PROXY_FAILURES).join("|"));
/** The error as the person should read it when their network's proxy was the problem; any other error as is. */
export function explainProxyRefusal(err: unknown, url: string): unknown {
const said = err instanceof Error ? err.message : String(err);
const code = PROXY_FAILURE.exec(said)?.[0] as keyof typeof PROXY_FAILURES | undefined;
if (!code) return err;
let host = "the site";
try {
host = new URL(url).host;
} catch {
// expected: an unparseable address is not echoed
}
return new Error(`${PROXY_FAILURES[code](host)} (${code})`, { cause: err });
}
export type CaptureGotoWaitUntil = "networkidle2" | "domcontentloaded";
export interface CaptureGotoOptions {
@@ -50,15 +74,16 @@ export async function navigateForCapture<TResponse>(
};
} catch (err) {
if (!isNavigationTimeoutError(err) || networkIdleTimeoutMs >= totalTimeoutMs) {
throw err;
throw explainProxyRefusal(err, url);
}
}
const fallbackTimeoutMs = totalTimeoutMs - networkIdleTimeoutMs;
const response = await page.goto(url, {
waitUntil: "domcontentloaded",
timeout: fallbackTimeoutMs,
});
const response = await page
.goto(url, { waitUntil: "domcontentloaded", timeout: fallbackTimeoutMs })
.catch((err: unknown) => {
throw explainProxyRefusal(err, url);
});
return {
response,
waitUntil: "domcontentloaded",