TryGhost/Ghost · error · Error
Unexpected external egress in worker ${workerInfo.workerInde
Error message
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. What it means
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.
Source
Thrown at e2e/helpers/playwright/fixture.ts:302
};
let serverHosts: string[] = [];
try {
const monitor = (await getEnvironmentManager()).getEgressMonitor();
if (monitor) {
serverHosts = await monitor.unexpectedHosts();
}
} catch {
// Best-effort: never fail teardown over a monitor read error.
}
const browserHosts = [...workerBrowserEgressHosts].sort();
print('resolved by Ghost', serverHosts);
print('requested by the browser', browserHosts);
const unexpected = [...new Set([...serverHosts, ...browserHosts])].sort();
if (EGRESS_ENFORCE && unexpected.length > 0) {
throw new Error(
`Unexpected external egress in worker ${workerInfo.workerIndex}: ${unexpected.join(', ')}\n\n` +
`Host(s) not on the egress allowlist were contacted during this worker's tests.\n` +
`If this is expected, add them to EGRESS_ALLOWLIST in helpers/environment/constants.ts.`
);
}
}, {
scope: 'worker',
auto: true
}],
_testEnvironmentContext: async ({config, isolation, labs, stripeEnabled, stripeServer, mailgunEnabled, mailgunServer}, use, testInfo: TestInfo) => {
const environmentManager = await getEnvironmentManager();
const requestedIsolation = getResolvedIsolation(testInfo, isolation);
// Stripe-enabled tests boot Ghost against a per-test fake Stripe server,
// so they cannot safely participate in per-file environment reuse.
const resolvedIsolation = stripeEnabled ? 'per-test' : requestedIsolation;
const suiteKey = getSuiteKey(testInfo);
const stripeConfig = stripeEnabled && stripeServer ? {View on GitHub (pinned to 47d8b0e2ad)
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.
Example fix
// before: test hits https://external.example.com
// after (option A — allowlist expected host)
// helpers/environment/constants.ts
EGRESS_ALLOWLIST.push('external.example.com');
// after (option B — stub the call)
await page.route('**/external.example.com/**', r => r.fulfill({status: 200, body: '{}'})); Defensive patterns
Strategy: validation
Validate before calling
function isAllowed(host: string): boolean {
return EGRESS_ALLOWLIST.some(a => host === a || host.endsWith('.' + a));
}
const unexpected = [...new Set([...serverHosts, ...browserHosts])].filter(h => !isAllowed(h));
if (EGRESS_ENFORCE && unexpected.length) throw new Error(`Unexpected egress: ${unexpected.join(', ')}`); Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Cannot create without a persistence adapter. Use buildMany()
- Failed to create ${entityType}: ${response.status()} ${error
- ${entityType} with id ${id} not found
- Failed to find ${entityType}: ${response.status()}
- Failed to update ${entityType}: ${response.status()}
AI-assisted analysis of TryGhost/Ghost@47d8b0e2ad (2026-08-13).
Data as JSON: /api/errors/ba3a4c8b6f9b2b7d.
Report an issue: GitHub.