Skip to content

Commit e50b5cc

Browse files
committed
Add regional egress interception repro
1 parent 298169f commit e50b5cc

4 files changed

Lines changed: 124 additions & 61 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@cloudflare/containers": patch
3+
---
4+
5+
Add deployed egress interception tests covering global and regional Worker placement.
Lines changed: 61 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { randomUUID } from 'node:crypto';
2-
import { describe, expect, vi } from 'vitest';
3-
import { test, WranglerDevRunner } from '../../test-helpers';
2+
import { describe, expect, test, vi } from 'vitest';
43

54
/**
65
* Egress interception tests.
@@ -14,142 +13,145 @@ import { test, WranglerDevRunner } from '../../test-helpers';
1413
* outbound = catch-all handler
1514
*/
1615
describe('egress interception', () => {
17-
describe('local', () => {
18-
async function proxyVia(
19-
runner: WranglerDevRunner,
20-
id: string,
21-
target: string
22-
): Promise<Response> {
23-
const url = await runner.getUrl();
24-
return vi.waitFor(
25-
async () => {
26-
const res = await fetch(`${url}/proxy?id=${id}&proxy=${encodeURIComponent(target)}`);
27-
if (res.status === 500 || res.status === 503) {
28-
throw new Error(`Container not ready, got ${res.status}`);
29-
}
30-
return res;
31-
},
32-
{ timeout: 15000 }
33-
);
16+
const baseUrl = process.env.EGRESS_TEST_BASE_URL;
17+
18+
if (!baseUrl) {
19+
throw new Error('EGRESS_TEST_BASE_URL must be set to the deployed Worker URL.');
20+
}
21+
22+
describe(process.env.EGRESS_TEST_ENV ?? 'deployed', () => {
23+
async function proxyVia(id: string, target: string): Promise<Response> {
24+
let lastResponse = '';
25+
try {
26+
return await vi.waitFor(
27+
async () => {
28+
const res = await fetch(`${baseUrl}/proxy?id=${id}&proxy=${encodeURIComponent(target)}`);
29+
if (res.status === 500 || res.status === 503) {
30+
lastResponse = await res.text();
31+
throw new Error(`Container not ready, got ${res.status}: ${lastResponse}`);
32+
}
33+
return res;
34+
},
35+
{ timeout: 120000 }
36+
);
37+
} catch (error) {
38+
if (lastResponse) {
39+
throw new Error(`${error instanceof Error ? error.message : String(error)}
40+
Last response: ${lastResponse}`);
41+
}
42+
throw error;
43+
}
3444
}
3545

36-
async function destroyContainer(runner: WranglerDevRunner, id: string) {
37-
// Tell the worker to destroy the container so the in-container onStop
38-
// hook can fire before the fixture tears down wrangler dev itself.
39-
// The full wrangler+workerd cleanup is handled automatically by the
40-
// `runner` test fixture.
41-
const url = await runner.getUrl();
42-
await fetch(`${url}/destroy?id=${id}`);
46+
async function destroyContainer(id: string) {
47+
await fetch(`${baseUrl}/destroy?id=${id}`);
4348
await new Promise(resolve => setTimeout(resolve, 1000));
4449
}
4550

46-
async function denyHost(runner: WranglerDevRunner, id: string, hostname: string) {
47-
const url = await runner.getUrl();
51+
async function denyHost(id: string, hostname: string) {
4852
const res = await fetch(
49-
`${url}/config/deny-host?id=${id}&hostname=${encodeURIComponent(hostname)}`
53+
`${baseUrl}/config/deny-host?id=${id}&hostname=${encodeURIComponent(hostname)}`
5054
);
5155
expect(res.status).toBe(200);
5256
}
5357

54-
test('deniedHosts blocks the request', async ({ runner }) => {
58+
test('deniedHosts blocks the request', async () => {
5559
const id = randomUUID();
5660

57-
const res = await proxyVia(runner, id, 'denied.com');
61+
const res = await proxyVia(id, 'denied.com');
5862
expect(res.status).toBe(520);
5963
const body = await res.text();
6064
expect(body).toContain('Origin is disallowed');
6165

62-
await destroyContainer(runner, id);
66+
await destroyContainer(id);
6367
});
6468

65-
test('allowedHosts gate blocks non-allowed hosts', async ({ runner }) => {
69+
test('allowedHosts gate blocks non-allowed hosts', async () => {
6670
const id = randomUUID();
6771

68-
const res = await proxyVia(runner, id, 'random.com');
72+
const res = await proxyVia(id, 'random.com');
6973
expect(res.status).toBe(520);
7074
const body = await res.text();
7175
expect(body).toContain('Origin is disallowed');
7276

73-
await destroyContainer(runner, id);
77+
await destroyContainer(id);
7478
});
7579

76-
test('outboundByHost handler is invoked for matching allowed host', async ({ runner }) => {
80+
test('outboundByHost handler is invoked for matching allowed host', async () => {
7781
const id = randomUUID();
7882

79-
const res = await proxyVia(runner, id, 'by-host.com');
83+
const res = await proxyVia(id, 'by-host.com');
8084
expect(res.status).toBe(200);
8185
const body = await res.text();
8286
expect(body).toBe('outboundByHost: by-host.com');
8387

84-
await destroyContainer(runner, id);
88+
await destroyContainer(id);
8589
});
8690

87-
test('catch-all outbound handler is invoked for allowed host without specific handler', async ({
88-
runner,
89-
}) => {
91+
test('catch-all outbound handler is invoked for allowed host without specific handler', async () => {
9092
const id = randomUUID();
9193

92-
const res = await proxyVia(runner, id, 'allowed.com');
94+
const res = await proxyVia(id, 'allowed.com');
9395
expect(res.status).toBe(200);
9496
const body = await res.text();
9597
expect(body).toBe('catch-all: allowed.com');
9698

97-
await destroyContainer(runner, id);
99+
await destroyContainer(id);
98100
});
99101

100-
test('denied host is blocked even if it would match allowedHosts', async ({ runner }) => {
102+
test('denied host is blocked even if it would match allowedHosts', async () => {
101103
const id = randomUUID();
102104

103-
const res = await proxyVia(runner, id, 'denied.com');
105+
const res = await proxyVia(id, 'denied.com');
104106
expect(res.status).toBe(520);
105107

106-
await destroyContainer(runner, id);
108+
await destroyContainer(id);
107109
});
108110

109-
test('glob pattern in outboundByHost matches subdomains', async ({ runner }) => {
111+
test('glob pattern in outboundByHost matches subdomains', async () => {
110112
const id = randomUUID();
111113

112-
const res = await proxyVia(runner, id, 'api.globtest.com');
114+
const res = await proxyVia(id, 'api.globtest.com');
113115
expect(res.status).toBe(200);
114116
const body = await res.text();
115117
expect(body).toBe('outboundByHost glob: api.globtest.com');
116118

117-
await destroyContainer(runner, id);
119+
await destroyContainer(id);
118120
});
119121

120-
test('glob pattern in outboundByHost matches deeply nested subdomains', async ({ runner }) => {
122+
test('glob pattern in outboundByHost matches deeply nested subdomains', async () => {
121123
const id = randomUUID();
122124

123-
const res = await proxyVia(runner, id, 'a.b.globtest.com');
125+
const res = await proxyVia(id, 'a.b.globtest.com');
124126
expect(res.status).toBe(200);
125127
const body = await res.text();
126128
expect(body).toBe('outboundByHost glob: a.b.globtest.com');
127129

128-
await destroyContainer(runner, id);
130+
await destroyContainer(id);
129131
});
130132

131-
test('glob pattern in allowedHosts blocks non-matching host', async ({ runner }) => {
133+
test('glob pattern in allowedHosts blocks non-matching host', async () => {
132134
const id = randomUUID();
133135

134136
// globtest.com itself does NOT match *.globtest.com
135-
const res = await proxyVia(runner, id, 'globtest.com');
137+
const res = await proxyVia(id, 'globtest.com');
136138
expect(res.status).toBe(520);
137139

138-
await destroyContainer(runner, id);
140+
await destroyContainer(id);
139141
});
140142

141-
test('denyHost also blocks the same hostname with a trailing dot', async ({ runner }) => {
143+
test('denyHost also blocks the same hostname with a trailing dot', async () => {
142144
const id = randomUUID();
143145
const hostname = `allowed-${randomUUID()}.example.com`;
144146

145-
await denyHost(runner, id, hostname);
147+
await denyHost(id, hostname);
146148

147-
const res = await proxyVia(runner, id, `${hostname}.`);
149+
const res = await proxyVia(id, `${hostname}.`);
148150
expect(res.status).toBe(520);
149151
const body = await res.text();
150152
expect(body).toContain('Origin is disallowed');
151153

152-
await destroyContainer(runner, id);
154+
await destroyContainer(id);
153155
});
154156
});
155157
});

examples/egress-tests/vitest.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@ import { defineConfig } from 'vitest/config';
22

33
export default defineConfig({
44
test: {
5-
testTimeout: 30000,
5+
testTimeout: 180000,
66
},
77
});

examples/egress-tests/wrangler.jsonc

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
{
22
"name": "egress-tests",
33
"main": "src/index.ts",
4+
"account_id": "3f8303a2f5df4f6e763e86bfba1a04ae",
45
"compatibility_date": "2026-05-12",
6+
"workers_dev": true,
57
"observability": {
68
"enabled": true,
79
},
@@ -10,7 +12,7 @@
1012
"image": "./Dockerfile",
1113
"class_name": "EgressTestContainer",
1214
"name": "egress-test-container",
13-
"max_instances": 2,
15+
"max_instances": 12,
1416
},
1517
],
1618
"durable_objects": {
@@ -27,4 +29,58 @@
2729
"new_sqlite_classes": ["EgressTestContainer"],
2830
},
2931
],
32+
"env": {
33+
"global": {
34+
"containers": [
35+
{
36+
"image": "./Dockerfile",
37+
"class_name": "EgressTestContainer",
38+
"name": "egress-test-container-global",
39+
"max_instances": 12,
40+
},
41+
],
42+
"durable_objects": {
43+
"bindings": [
44+
{
45+
"class_name": "EgressTestContainer",
46+
"name": "CONTAINER",
47+
},
48+
],
49+
},
50+
"migrations": [
51+
{
52+
"tag": "v1",
53+
"new_sqlite_classes": ["EgressTestContainer"],
54+
},
55+
],
56+
},
57+
"regional": {
58+
"placement": {
59+
"mode": "targeted",
60+
"region": "aws:us-east-1",
61+
},
62+
"containers": [
63+
{
64+
"image": "./Dockerfile",
65+
"class_name": "EgressTestContainer",
66+
"name": "egress-test-container-regional",
67+
"max_instances": 12,
68+
},
69+
],
70+
"durable_objects": {
71+
"bindings": [
72+
{
73+
"class_name": "EgressTestContainer",
74+
"name": "CONTAINER",
75+
},
76+
],
77+
},
78+
"migrations": [
79+
{
80+
"tag": "v1",
81+
"new_sqlite_classes": ["EgressTestContainer"],
82+
},
83+
],
84+
},
85+
},
3086
}

0 commit comments

Comments
 (0)