mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 09:16:55 +00:00
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:
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user