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

  1. If the host is expected, add it to EGRESS_ALLOWLIST in helpers/environment/constants.ts.
  2. If unexpected, remove the offending call from the test/feature — hermetic tests must not hit it.
  3. Inspect both 'resolved by Ghost' and 'requested by the browser' lists in the output to find the source.
  4. 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

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


AI-assisted analysis of TryGhost/Ghost@47d8b0e2ad (2026-08-13). Data as JSON: /api/errors/ba3a4c8b6f9b2b7d. Report an issue: GitHub.