diff --git a/packages/safe-chain/src/registryProxy/mitmRequestHandler.js b/packages/safe-chain/src/registryProxy/mitmRequestHandler.js index 82685594..9d3388d1 100644 --- a/packages/safe-chain/src/registryProxy/mitmRequestHandler.js +++ b/packages/safe-chain/src/registryProxy/mitmRequestHandler.js @@ -67,21 +67,29 @@ function createHttpsServer(hostname, port, interceptor) { return; } - const pathAndQuery = getRequestPathAndQuery(req.url); - const targetUrl = `https://${hostname}${pathAndQuery}`; - - const requestInterceptor = await interceptor.handleRequest(targetUrl); - const blockResponse = requestInterceptor.blockResponse; + try { + const pathAndQuery = getRequestPathAndQuery(req.url); + const targetUrl = `https://${hostname}${pathAndQuery}`; + + const requestInterceptor = await interceptor.handleRequest(targetUrl); + const blockResponse = requestInterceptor.blockResponse; + + if (blockResponse) { + ui.writeVerbose(`Safe-chain: Blocking request to ${targetUrl}`); + res.writeHead(blockResponse.statusCode, blockResponse.message); + res.end(blockResponse.message); + return; + } - if (blockResponse) { - ui.writeVerbose(`Safe-chain: Blocking request to ${targetUrl}`); - res.writeHead(blockResponse.statusCode, blockResponse.message); - res.end(blockResponse.message); - return; + // Collect request body + forwardRequest(req, hostname, port, res, requestInterceptor); + } catch (/** @type {any} */ error) { + ui.writeError( + `Safe-chain: Error handling request for ${req.url}: ${error.message}` + ); + res.writeHead(502, "Bad Gateway"); + res.end("Bad Gateway: Error handling request"); } - - // Collect request body - forwardRequest(req, hostname, port, res, requestInterceptor); } const server = https.createServer( diff --git a/packages/safe-chain/src/registryProxy/tunnelRequestHandler.js b/packages/safe-chain/src/registryProxy/tunnelRequestHandler.js index 5eac3816..c64de509 100644 --- a/packages/safe-chain/src/registryProxy/tunnelRequestHandler.js +++ b/packages/safe-chain/src/registryProxy/tunnelRequestHandler.js @@ -14,22 +14,28 @@ let timedoutImdsEndpoints = []; * @returns {void} */ export function tunnelRequest(req, clientSocket, head) { - const httpsProxy = process.env.HTTPS_PROXY || process.env.https_proxy; - - if (httpsProxy) { - // If an HTTPS proxy is set, tunnel the request via the proxy - // This is the system proxy, not the safe-chain proxy - // The package manager will run via the safe-chain proxy - // The safe-chain proxy will then send the request to the system proxy - // Typical flow: package manager -> safe-chain proxy -> system proxy -> destination - - // There are 2 processes involved in this: - // 1. Safe-chain process: has HTTPS_PROXY set to system proxy - // 2. Package manager process: has HTTPS_PROXY set to safe-chain proxy - - tunnelRequestViaProxy(req, clientSocket, head, httpsProxy); - } else { - tunnelRequestToDestination(req, clientSocket, head); + try { + const httpsProxy = process.env.HTTPS_PROXY || process.env.https_proxy; + + if (httpsProxy) { + // If an HTTPS proxy is set, tunnel the request via the proxy + // This is the system proxy, not the safe-chain proxy + // The package manager will run via the safe-chain proxy + // The safe-chain proxy will then send the request to the system proxy + // Typical flow: package manager -> safe-chain proxy -> system proxy -> destination + + // There are 2 processes involved in this: + // 1. Safe-chain process: has HTTPS_PROXY set to system proxy + // 2. Package manager process: has HTTPS_PROXY set to safe-chain proxy + + tunnelRequestViaProxy(req, clientSocket, head, httpsProxy); + } else { + tunnelRequestToDestination(req, clientSocket, head); + } + } catch (/** @type {any} */ err) { + ui.writeError( + `Safe-chain: tunnel request failed for ${req.url} : ${err.message}` + ); } } diff --git a/test/e2e/dns-failure-resilience.e2e.spec.js b/test/e2e/dns-failure-resilience.e2e.spec.js new file mode 100644 index 00000000..5504f1d8 --- /dev/null +++ b/test/e2e/dns-failure-resilience.e2e.spec.js @@ -0,0 +1,54 @@ +import { describe, it, before, beforeEach, afterEach } from "node:test"; +import { DockerTestContainer } from "./DockerTestContainer.js"; +import assert from "node:assert"; + +describe("E2E: DNS failure resilience", () => { + let container; + + before(async () => { + DockerTestContainer.buildImage(); + }); + + beforeEach(async () => { + container = new DockerTestContainer(); + await container.start(); + + const installationShell = await container.openShell("zsh"); + await installationShell.runCommand("safe-chain setup"); + }); + + afterEach(async () => { + if (container) { + await container.stop(); + container = null; + } + }); + + it("should not crash when the npm registry is unreachable", async () => { + const shell = await container.openShell("zsh"); + + // Make the npm registry domain unreachable. + // `npm install lodash` talks to https://registry.npmjs.org/ for both metadata and tarballs. + await shell.runCommand( + 'echo "127.0.0.1 registry.npmjs.org" >> /etc/hosts' + ); + + const result = await shell.runCommand( + // Fail fast so the shell runner doesn't time out. + // Also disable extra network calls that could introduce noise. + "npm install lodash --no-audit --no-fund --fetch-retries=0 --fetch-timeout=2000 --safe-chain-logging=verbose" + ); + + assert.ok( + result.output.includes("registry.npmjs.org"), + `Output did not reference the npm registry host; /etc/hosts override may not have applied. Output was:\n${result.output}` + ); + + // Ensure it did NOT crash with Unhandled Promise Rejection + assert.strictEqual( + result.output.includes("Unhandled promise rejection"), + false, + `Output indicates process crash (Unhandled promise rejection). Output was:\n${result.output}` + ); + }); +});