{"record":{"id":"ba3a4c8b6f9b2b7d","repo":"TryGhost/Ghost","slug":"unexpected-external-egress-in-worker-workerinfo","errorCode":null,"errorMessage":"Unexpected external egress in worker ${workerInfo.workerIndex}: ${unexpected.join(', ')}\\n\\nHost(s) not on the egress allowlist were contacted during this worker's tests.\\nIf this is expected, add them to EGRESS_ALLOWLIST in helpers/environment/constants.ts.","messagePattern":"Unexpected external egress in worker (.+?): (.+?)\\\\n\\\\nHost\\(s\\) not on the egress allowlist were contacted during this worker's tests\\.\\\\nIf this is expected, add them to EGRESS_ALLOWLIST in helpers/environment/constants\\.ts\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"e2e/helpers/playwright/fixture.ts","lineNumber":302,"sourceCode":"        };\n\n        let serverHosts: string[] = [];\n        try {\n            const monitor = (await getEnvironmentManager()).getEgressMonitor();\n            if (monitor) {\n                serverHosts = await monitor.unexpectedHosts();\n            }\n        } catch {\n            // Best-effort: never fail teardown over a monitor read error.\n        }\n        const browserHosts = [...workerBrowserEgressHosts].sort();\n\n        print('resolved by Ghost', serverHosts);\n        print('requested by the browser', browserHosts);\n\n        const unexpected = [...new Set([...serverHosts, ...browserHosts])].sort();\n        if (EGRESS_ENFORCE && unexpected.length > 0) {\n            throw new Error(\n                `Unexpected external egress in worker ${workerInfo.workerIndex}: ${unexpected.join(', ')}\\n\\n` +\n                `Host(s) not on the egress allowlist were contacted during this worker's tests.\\n` +\n                `If this is expected, add them to EGRESS_ALLOWLIST in helpers/environment/constants.ts.`\n            );\n        }\n    }, {\n        scope: 'worker',\n        auto: true\n    }],\n\n    _testEnvironmentContext: async ({config, isolation, labs, stripeEnabled, stripeServer, mailgunEnabled, mailgunServer}, use, testInfo: TestInfo) => {\n        const environmentManager = await getEnvironmentManager();\n        const requestedIsolation = getResolvedIsolation(testInfo, isolation);\n        // Stripe-enabled tests boot Ghost against a per-test fake Stripe server,\n        // so they cannot safely participate in per-file environment reuse.\n        const resolvedIsolation = stripeEnabled ? 'per-test' : requestedIsolation;\n        const suiteKey = getSuiteKey(testInfo);\n        const stripeConfig = stripeEnabled && stripeServer ? {","sourceCodeStart":284,"sourceCodeEnd":320,"githubUrl":"https://github.com/TryGhost/Ghost/blob/47d8b0e2ad2fd4757d3bc45f46c3ac165ff8a1fe/e2e/helpers/playwright/fixture.ts#L284-L320","documentation":"Worker teardown computes unexpected external egress: the union of hosts resolved by the egress monitor (server-side DNS lookups) and hosts requested by the browser, minus the EGRESS_ALLOWLIST. If EGRESS_ENFORCE is on and any unexpected host remains, the worker fails. This is an intentional hermetic-network guard — Ghost e2e tests must not reach the public internet.","triggerScenarios":"Test code or a Ghost feature made an outbound call to a host not on EGRESS_ALLOWLIST. A new third-party integration shipped and its endpoints aren't allowlisted. Browser fetched an external resource (font, analytics, image). DNS leaked to an unexpected resolver host.","commonSituations":"Adding a feature that calls an external API without updating EGRESS_ALLOWLIST; analytics/CDN resources loaded by a theme; a flaky DNS resolution landing on an unexpected host; version bump enabling a new telemetry endpoint.","solutions":["If the host is expected, add it to EGRESS_ALLOWLIST in helpers/environment/constants.ts.","If unexpected, remove the offending call from the test/feature — hermetic tests must not hit it.","Inspect both 'resolved by Ghost' and 'requested by the browser' lists in the output to find the source.","Temporarily set EGRESS_ENFORCE=false to debug, then re-enable once allowlisted or removed."],"exampleFix":"// before: test hits https://external.example.com\n\n// after (option A — allowlist expected host)\n// helpers/environment/constants.ts\nEGRESS_ALLOWLIST.push('external.example.com');\n\n// after (option B — stub the call)\nawait page.route('**/external.example.com/**', r => r.fulfill({status: 200, body: '{}'}));","handlingStrategy":"validation","validationCode":"function isAllowed(host: string): boolean {\n    return EGRESS_ALLOWLIST.some(a => host === a || host.endsWith('.' + a));\n}\nconst unexpected = [...new Set([...serverHosts, ...browserHosts])].filter(h => !isAllowed(h));\nif (EGRESS_ENFORCE && unexpected.length) throw new Error(`Unexpected egress: ${unexpected.join(', ')}`);","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Keep EGRESS_ALLOWLIST in sync with every external host any feature legitimately contacts.","Stub external calls via page.route/request interception in tests instead of hitting real hosts.","Use EGRESS_ENFORCE=false only for debugging; always re-enable before merging."],"tags":["e2e","egress","hermetic-tests","security","allowlist"],"backgroundTag":null,"analyzedSha":"47d8b0e2ad2fd4757d3bc45f46c3ac165ff8a1fe","analyzedAt":"2026-08-13T01:25:26.651Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}