Skip to content

Commit b61841c

Browse files
Fix WebSocket connections through exposed ports (#156)
* Add WebSocket support for exposed sandbox ports WebSocket upgrade requests previously failed because containerFetch() uses JSRPC which cannot serialize the upgrade handshake. WebSocket requests now route through Container's fetch() instead. * Add E2E test for WebSocket connections * Add changeset
1 parent 3ef0f35 commit b61841c

File tree

10 files changed

+623
-14
lines changed

10 files changed

+623
-14
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@cloudflare/sandbox": patch
3+
---
4+
5+
Fix WebSocket upgrade requests through exposed ports

package-lock.json

Lines changed: 60 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,20 +33,22 @@
3333
"@types/node": "^24.1.0",
3434
"@types/react": "^19.1.8",
3535
"@types/react-dom": "^19.1.6",
36+
"@types/ws": "^8.18.1",
3637
"@vitejs/plugin-react": "^4.7.0",
3738
"@vitest/ui": "^3.2.4",
3839
"fast-glob": "^3.3.3",
3940
"happy-dom": "^20.0.0",
41+
"pkg-pr-new": "^0.0.60",
4042
"react": "^19.1.0",
4143
"react-dom": "^19.1.0",
4244
"tsup": "^8.5.0",
4345
"tsx": "^4.20.3",
4446
"turbo": "^2.5.8",
4547
"typescript": "^5.8.3",
46-
"pkg-pr-new": "^0.0.60",
4748
"vite": "^7.1.11",
4849
"vitest": "^3.2.4",
49-
"wrangler": "^4.42.2"
50+
"wrangler": "^4.42.2",
51+
"ws": "^8.18.3"
5052
},
5153
"private": true,
5254
"packageManager": "[email protected]"

packages/sandbox/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
"docker:local": "cd ../.. && docker build -f packages/sandbox/Dockerfile --build-arg SANDBOX_VERSION=$npm_package_version -t cloudflare/sandbox-test:$npm_package_version .",
2828
"docker:publish": "cd ../.. && docker buildx build --platform linux/amd64,linux/arm64 -f packages/sandbox/Dockerfile --build-arg SANDBOX_VERSION=$npm_package_version -t cloudflare/sandbox:$npm_package_version --push .",
2929
"docker:publish:beta": "cd ../.. && docker buildx build --platform linux/amd64,linux/arm64 -f packages/sandbox/Dockerfile --build-arg SANDBOX_VERSION=$npm_package_version -t cloudflare/sandbox:$npm_package_version-beta --push .",
30-
"test": "vitest run --config vitest.config.ts",
30+
"test": "vitest run --config vitest.config.ts \"$@\"",
3131
"test:e2e": "cd ../.. && vitest run --config vitest.e2e.config.ts \"$@\""
3232
},
3333
"exports": {

packages/sandbox/src/request-handler.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { createLogger, type LogContext, TraceContext } from "@repo/shared";
2+
import { switchPort } from "@cloudflare/containers";
23
import { getSandbox, type Sandbox } from "./sandbox";
34
import {
45
sanitizeSandboxId,
@@ -70,6 +71,14 @@ export async function proxyToSandbox<E extends SandboxEnv>(
7071
}
7172
}
7273

74+
// Detect WebSocket upgrade request
75+
const upgradeHeader = request.headers.get('Upgrade');
76+
if (upgradeHeader?.toLowerCase() === 'websocket') {
77+
// WebSocket path: Must use fetch() not containerFetch()
78+
// This bypasses JSRPC serialization boundary which cannot handle WebSocket upgrades
79+
return await sandbox.fetch(switchPort(request, port));
80+
}
81+
7382
// Build proxy request with proper headers
7483
let proxyUrl: string;
7584

@@ -96,7 +105,7 @@ export async function proxyToSandbox<E extends SandboxEnv>(
96105
duplex: 'half',
97106
});
98107

99-
return sandbox.containerFetch(proxyRequest, port);
108+
return await sandbox.containerFetch(proxyRequest, port);
100109
} catch (error) {
101110
logger.error('Proxy routing error', error instanceof Error ? error : new Error(String(error)));
102111
return new Response('Proxy routing error', { status: 500 });

packages/sandbox/src/sandbox.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,17 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
238238
await this.ctx.storage.put('sandboxName', name);
239239
}
240240

241-
// Determine which port to route to
241+
// Detect WebSocket upgrade request
242+
const upgradeHeader = request.headers.get('Upgrade');
243+
const isWebSocket = upgradeHeader?.toLowerCase() === 'websocket';
244+
245+
if (isWebSocket) {
246+
// WebSocket path: Let parent Container class handle WebSocket proxying
247+
// This bypasses containerFetch() which uses JSRPC and cannot handle WebSocket upgrades
248+
return await super.fetch(request);
249+
}
250+
251+
// Non-WebSocket: Use existing port determination and HTTP routing logic
242252
const port = this.determinePort(url);
243253

244254
// Route to the appropriate port

0 commit comments

Comments
 (0)