Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-websocket-routing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cloudflare/sandbox": patch
---

Fix WebSocket upgrade requests through exposed ports
64 changes: 60 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,20 +33,22 @@
"@types/node": "^24.1.0",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@types/ws": "^8.18.1",
"@vitejs/plugin-react": "^4.7.0",
"@vitest/ui": "^3.2.4",
"fast-glob": "^3.3.3",
"happy-dom": "^20.0.0",
"pkg-pr-new": "^0.0.60",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"tsup": "^8.5.0",
"tsx": "^4.20.3",
"turbo": "^2.5.8",
"typescript": "^5.8.3",
"pkg-pr-new": "^0.0.60",
"vite": "^7.1.11",
"vitest": "^3.2.4",
"wrangler": "^4.42.2"
"wrangler": "^4.42.2",
"ws": "^8.18.3"
},
"private": true,
"packageManager": "[email protected]"
Expand Down
2 changes: 1 addition & 1 deletion packages/sandbox/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"docker:local": "cd ../.. && docker build -f packages/sandbox/Dockerfile --build-arg SANDBOX_VERSION=$npm_package_version -t cloudflare/sandbox-test:$npm_package_version .",
"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 .",
"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 .",
"test": "vitest run --config vitest.config.ts",
"test": "vitest run --config vitest.config.ts \"$@\"",
"test:e2e": "cd ../.. && vitest run --config vitest.e2e.config.ts \"$@\""
},
"exports": {
Expand Down
11 changes: 10 additions & 1 deletion packages/sandbox/src/request-handler.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { createLogger, type LogContext, TraceContext } from "@repo/shared";
import { switchPort } from "@cloudflare/containers";
import { getSandbox, type Sandbox } from "./sandbox";
import {
sanitizeSandboxId,
Expand Down Expand Up @@ -70,6 +71,14 @@ export async function proxyToSandbox<E extends SandboxEnv>(
}
}

// Detect WebSocket upgrade request
const upgradeHeader = request.headers.get('Upgrade');
if (upgradeHeader?.toLowerCase() === 'websocket') {
// WebSocket path: Must use fetch() not containerFetch()
// This bypasses JSRPC serialization boundary which cannot handle WebSocket upgrades
return await sandbox.fetch(switchPort(request, port));
}

// Build proxy request with proper headers
let proxyUrl: string;

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

return sandbox.containerFetch(proxyRequest, port);
return await sandbox.containerFetch(proxyRequest, port);
} catch (error) {
logger.error('Proxy routing error', error instanceof Error ? error : new Error(String(error)));
return new Response('Proxy routing error', { status: 500 });
Expand Down
12 changes: 11 additions & 1 deletion packages/sandbox/src/sandbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,17 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
await this.ctx.storage.put('sandboxName', name);
}

// Determine which port to route to
// Detect WebSocket upgrade request
const upgradeHeader = request.headers.get('Upgrade');
const isWebSocket = upgradeHeader?.toLowerCase() === 'websocket';

if (isWebSocket) {
// WebSocket path: Let parent Container class handle WebSocket proxying
// This bypasses containerFetch() which uses JSRPC and cannot handle WebSocket upgrades
return await super.fetch(request);
}

// Non-WebSocket: Use existing port determination and HTTP routing logic
const port = this.determinePort(url);

// Route to the appropriate port
Expand Down
Loading