Skip to content

Commit 9d9d60b

Browse files
committed
feat: add WebSocket support for sandbox proxy
- Added WebSocket upgrade request handling in sandbox proxy to support real-time connections - Modified proxy logic to bypass JSRPC serialization for WebSocket requests using direct fetch - Updated dependency versions including @cloudflare/sandbox to 0.4.14 for improved WebSocket support - Enhanced request handler to properly detect and route WebSocket upgrade headers - Added logging for WebSocket upgrade requests to improve debugging
1 parent fd70b1c commit 9d9d60b

4 files changed

Lines changed: 37 additions & 15 deletions

File tree

bun.lock

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

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
"dependencies": {
3535
"@ashishkumar472/cf-git": "1.0.4",
3636
"@cloudflare/containers": "^0.0.28",
37-
"@cloudflare/sandbox": "0.4.7",
37+
"@cloudflare/sandbox": "0.4.14",
3838
"@noble/ciphers": "^1.3.0",
3939
"@octokit/rest": "^22.0.0",
4040
"@radix-ui/react-accordion": "^1.2.12",

worker/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@ async function handleUserAppRequest(request: Request, env: Env): Promise<Respons
5151
const sandboxResponse = await proxyToSandbox(request, env);
5252
if (sandboxResponse) {
5353
logger.info(`Serving response from sandbox for: ${hostname}`);
54+
// If it was a websocket upgrade, we need to return the response as is
55+
if (sandboxResponse.headers.get('Upgrade')?.toLowerCase() === 'websocket') {
56+
logger.info(`Serving websocket response from sandbox for: ${hostname}`);
57+
return sandboxResponse;
58+
}
5459

5560
// Add headers to identify this as a sandbox response
5661
let headers = new Headers(sandboxResponse.headers);

worker/services/sandbox/request-handler.ts

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ This code is borrowed from Cloudflare Sandbox-sdk's npm package
44

55
import { createObjectLogger } from "../../logger";
66
import { getSandbox, type Sandbox } from "@cloudflare/sandbox";
7+
import { switchPort } from '@cloudflare/containers';
78

89
const logger = createObjectLogger({
910
component: 'sandbox-do',
@@ -36,6 +37,17 @@ export async function proxyToSandbox<E extends SandboxEnv>(
3637
const { sandboxId, port, path, token } = routeInfo;
3738
const sandbox = getSandbox(env.Sandbox, sandboxId);
3839

40+
logger.info("[Proxy] Sandbox", sandbox, "Port", port, "Path", path, "Token", token);
41+
42+
// Detect WebSocket upgrade request
43+
const upgradeHeader = request.headers.get('Upgrade');
44+
if (upgradeHeader?.toLowerCase() === 'websocket') {
45+
logger.info("[Proxy] WebSocket upgrade request", upgradeHeader, "Port", port, "Path", path, "Token", token);
46+
// WebSocket path: Must use fetch() not containerFetch()
47+
// This bypasses JSRPC serialization boundary which cannot handle WebSocket upgrades
48+
return await sandbox.fetch(switchPort(request, port));
49+
}
50+
3951
// Build proxy request with proper headers
4052
let proxyUrl: string;
4153

@@ -55,7 +67,7 @@ export async function proxyToSandbox<E extends SandboxEnv>(
5567
'X-Original-URL': request.url,
5668
'X-Forwarded-Host': url.hostname,
5769
'X-Forwarded-Proto': url.protocol.replace(':', ''),
58-
'X-Sandbox-Name': sandboxId, // Pass the friendly name
70+
'X-Sandbox-Name': sandboxId // Pass the friendly name
5971
},
6072
body: request.body,
6173
// @ts-expect-error - duplex required for body streaming in modern runtimes
@@ -70,17 +82,22 @@ export async function proxyToSandbox<E extends SandboxEnv>(
7082
proxyUrl,
7183
});
7284

73-
return sandbox.containerFetch(proxyRequest, port);
85+
return await sandbox.containerFetch(proxyRequest, port);
7486
} catch (error) {
75-
logger.error('Proxy routing error', error instanceof Error ? error : new Error(String(error)));
87+
logger.error(
88+
'Proxy routing error',
89+
error instanceof Error ? error : new Error(String(error))
90+
);
7691
return new Response('Proxy routing error', { status: 500 });
7792
}
7893
}
7994

8095
function extractSandboxRoute(url: URL): RouteInfo | null {
8196
// Parse subdomain pattern: port-sandboxId-token.domain (tokens mandatory)
8297
// Token is always exactly 16 chars (generated by generatePortToken)
83-
const subdomainMatch = url.hostname.match(/^(\d{4,5})-([^.-][^.]*?[^.-]|[^.-])-([a-z0-9_-]{16})\.(.+)$/);
98+
const subdomainMatch = url.hostname.match(
99+
/^(\d{4,5})-([^.-][^.]*?[^.-]|[^.-])-([a-z0-9_-]{16})\.(.+)$/
100+
);
84101

85102
if (!subdomainMatch) {
86103
return null;
@@ -117,18 +134,18 @@ export function isLocalhostPattern(hostname: string): boolean {
117134
return hostname === '[::1]';
118135
}
119136
}
120-
137+
121138
// Handle bare IPv6 without brackets
122139
if (hostname === '::1') {
123140
return true;
124141
}
125-
142+
126143
// For IPv4 and regular hostnames, split on colon to remove port
127-
const hostPart = hostname.split(":")[0];
128-
144+
const hostPart = hostname.split(':')[0];
145+
129146
return (
130-
hostPart === "localhost" ||
131-
hostPart === "127.0.0.1" ||
132-
hostPart === "0.0.0.0"
147+
hostPart === 'localhost' ||
148+
hostPart === '127.0.0.1' ||
149+
hostPart === '0.0.0.0'
133150
);
134151
}

0 commit comments

Comments
 (0)