diff --git a/src/content/docs/sandbox/api/ports.mdx b/src/content/docs/sandbox/api/ports.mdx index 8506226d73bfd03..5c98b5bdd67b0a4 100644 --- a/src/content/docs/sandbox/api/ports.mdx +++ b/src/content/docs/sandbox/api/ports.mdx @@ -86,19 +86,22 @@ console.log(`${port.name || port.port}: ${port.exposedAt}`); ``` -### `connect()` +### `wsConnect()` -Route incoming WebSocket upgrade requests to WebSocket servers running in the sandbox. +Connect to WebSocket servers running in the sandbox. Use this when your Worker needs to establish WebSocket connections with services in the sandbox. -```ts -import { connect } from '@cloudflare/sandbox'; +**Common use cases:** +- Route incoming WebSocket upgrade requests with custom authentication or authorization +- Connect from your Worker to get real-time data from sandbox services + +For exposing WebSocket services via public preview URLs, use `exposePort()` with `proxyToSandbox()` instead. See [WebSocket Connections guide](/sandbox/guides/websocket-connections/) for examples. -const response = await connect(sandbox: Sandbox, request: Request, port: number): Promise +```ts +const response = await sandbox.wsConnect(request: Request, port: number): Promise ``` **Parameters**: -- `sandbox` - Sandbox instance containing the WebSocket server - `request` - Incoming WebSocket upgrade request - `port` - Port number (1024-65535, excluding 3000) @@ -106,53 +109,18 @@ const response = await connect(sandbox: Sandbox, request: Request, port: number) ```ts -import { getSandbox, connect, type Sandbox } from "@cloudflare/sandbox"; - -export { Sandbox } from "@cloudflare/sandbox"; - -type Env = { -Sandbox: DurableObjectNamespace; -}; - -const initialized = new Set(); +import { getSandbox } from "@cloudflare/sandbox"; export default { async fetch(request: Request, env: Env): Promise { - const sandboxId = "my-sandbox"; - const sandbox = getSandbox(env.Sandbox, sandboxId); - if (request.headers.get('Upgrade')?.toLowerCase() === 'websocket') { - // Initialize WebSocket server on first connection - if (!initialized.has(sandboxId)) { - const serverCode = ` - -Bun.serve({ -port: 8080, -fetch(req, server) { -if (server.upgrade(req)) return; -return new Response('WebSocket server', { status: 200 }); -}, -websocket: { -message(ws, msg) { -ws.send(\`Echo: \${msg}\`); -} -} -}); -`; -await sandbox.writeFile('/workspace/server.js', serverCode); -await sandbox.startProcess('bun /workspace/server.js'); -await new Promise(resolve => setTimeout(resolve, 1000)); -initialized.add(sandboxId); -} - - return await connect(sandbox, request, 8080); + const sandbox = getSandbox(env.Sandbox, 'my-sandbox'); + return await sandbox.wsConnect(request, 8080); } - return new Response('WebSocket endpoint. Connect using ws:// protocol.', { status: 200 }); - -}, + return new Response('WebSocket endpoint', { status: 200 }); + } }; - ``` diff --git a/src/content/docs/sandbox/concepts/preview-urls.mdx b/src/content/docs/sandbox/concepts/preview-urls.mdx index 410f446c4948ed8..8ba00fadf8da3cb 100644 --- a/src/content/docs/sandbox/concepts/preview-urls.mdx +++ b/src/content/docs/sandbox/concepts/preview-urls.mdx @@ -71,6 +71,7 @@ const admin = await sandbox.exposePort(3001, { name: "admin" }); ## What Works - HTTP/HTTPS requests +- WebSocket connections - Server-Sent Events - All HTTP methods (GET, POST, PUT, DELETE, etc.) - Request and response headers @@ -82,6 +83,28 @@ const admin = await sandbox.exposePort(3001, { name: "admin" }); - Ports outside range 1024-65535 - Port 3000 (used internally by the SDK) +## WebSocket Support + +Preview URLs support WebSocket connections. When a WebSocket upgrade request hits an exposed port, the routing layer automatically handles the connection handshake. + +```typescript +// Start a WebSocket server +await sandbox.startProcess("bun run ws-server.ts 8080"); +const { exposedAt } = await sandbox.exposePort(8080); + +// Clients connect using WebSocket protocol +// Browser: new WebSocket('wss://8080-abc123.example.com') + +// Your Worker routes automatically +export default { + async fetch(request, env) { + return proxyToSandbox(request, env.Sandbox, "sandbox-id"); + }, +}; +``` + +For custom routing scenarios where your Worker needs to control which sandbox or port to connect to based on request properties, see `wsConnect()` in the [Ports API](/sandbox/api/ports/#wsconnect). + ## Security :::caution diff --git a/src/content/docs/sandbox/guides/websocket-connections.mdx b/src/content/docs/sandbox/guides/websocket-connections.mdx new file mode 100644 index 000000000000000..ee4964a716a465b --- /dev/null +++ b/src/content/docs/sandbox/guides/websocket-connections.mdx @@ -0,0 +1,216 @@ +--- +title: WebSocket Connections +pcx_content_type: how-to +sidebar: + order: 5 +description: Connect to WebSocket servers running in sandboxes. +--- + +import { TypeScriptExample } from "~/components"; + +This guide shows you how to work with WebSocket servers running in your sandboxes. + +## Choose your approach + +**Expose via preview URL** - Get a public URL for external clients to connect to. Best for public chat rooms, multiplayer games, or real-time dashboards. + +**Connect with wsConnect()** - Your Worker establishes the WebSocket connection. Best for custom routing logic, authentication gates, or when your Worker needs real-time data from sandbox services. + +## Connect to WebSocket echo server + +**Create the echo server:** + +```typescript title="echo-server.ts" +Bun.serve({ + port: 8080, + hostname: "0.0.0.0", + fetch(req, server) { + if (server.upgrade(req)) { + return; + } + return new Response("WebSocket echo server"); + }, + websocket: { + message(ws, message) { + ws.send(`Echo: ${message}`); + }, + open(ws) { + console.log("Client connected"); + }, + close(ws) { + console.log("Client disconnected"); + }, + }, +}); + +console.log("WebSocket server listening on port 8080"); +``` + +**Extend the Dockerfile:** + +```dockerfile title="Dockerfile" +FROM docker.io/cloudflare/sandbox:0.3.3 + +# Copy echo server into the container +COPY echo-server.ts /workspace/echo-server.ts + +# Create custom startup script +COPY startup.sh /container-server/startup.sh +RUN chmod +x /container-server/startup.sh +``` + +**Create startup script:** + +```bash title="startup.sh" +#!/bin/bash +# Start your WebSocket server in the background +bun /workspace/echo-server.ts & +# Start SDK's control plane (needed for the SDK to work) +exec bun dist/index.js +``` + +**Connect from your Worker:** + + +```ts +import { getSandbox } from '@cloudflare/sandbox'; + +export { Sandbox } from "@cloudflare/sandbox"; + +export default { + async fetch(request: Request, env: Env): Promise { + if (request.headers.get('Upgrade')?.toLowerCase() === 'websocket') { + const sandbox = getSandbox(env.Sandbox, 'echo-service'); + return await sandbox.wsConnect(request, 8080); + } + + return new Response('WebSocket endpoint'); + +} +}; + +```` + + +**Client connects:** + +```javascript +const ws = new WebSocket('wss://your-worker.com'); +ws.onmessage = (event) => console.log(event.data); +ws.send('Hello!'); // Receives: "Echo: Hello!" +```` + +## Expose WebSocket service via preview URL + +Get a public URL for your WebSocket server: + + +```ts +import { getSandbox, proxyToSandbox } from '@cloudflare/sandbox'; + +export default { + async fetch(request: Request, env: Env): Promise { + const sandbox = getSandbox(env.Sandbox, 'echo-service'); + + // Expose the port to get preview URL + const { exposedAt } = await sandbox.exposePort(8080); + + // Return URL to clients + if (request.url.includes('/ws-url')) { + return Response.json({ url: exposedAt.replace('https', 'wss') }); + } + + // Auto-route all requests via proxyToSandbox + return proxyToSandbox(request, env.Sandbox, 'echo-service'); + +} +}; + +```` + + +**Client connects to preview URL:** + +```javascript +// Get the preview URL +const response = await fetch('https://your-worker.com/ws-url'); +const { url } = await response.json(); + +// Connect +const ws = new WebSocket(url); +ws.onmessage = (event) => console.log(event.data); +ws.send('Hello!'); // Receives: "Echo: Hello!" +```` + +## Connect from Worker to get real-time data + +Your Worker can connect to a WebSocket service to get real-time data, even when the incoming request isn't a WebSocket: + + +```ts +export default { + async fetch(request: Request, env: Env): Promise { + const sandbox = getSandbox(env.Sandbox, 'data-processor'); + + // Incoming HTTP request needs real-time data from sandbox + const wsRequest = new Request('ws://internal', { + headers: { + 'Upgrade': 'websocket', + 'Connection': 'Upgrade' + } + }); + + // Connect to WebSocket service in sandbox + const wsResponse = await sandbox.wsConnect(wsRequest, 8080); + + // Process WebSocket stream and return HTTP response + // (Implementation depends on your needs) + + return new Response('Processed real-time data'); + +} +}; + +```` + + +This pattern is useful when you need streaming data from sandbox services but want to return HTTP responses to clients. + +## Troubleshooting + +### Upgrade failed + +Verify request has WebSocket headers: + + +```ts +console.log(request.headers.get('Upgrade')); // 'websocket' +console.log(request.headers.get('Connection')); // 'Upgrade' +```` + + + +### Local development + +Expose ports in Dockerfile for `wrangler dev`: + +```dockerfile title="Dockerfile" +FROM docker.io/cloudflare/sandbox:0.3.3 + +COPY echo-server.ts /workspace/echo-server.ts +COPY startup.sh /container-server/startup.sh +RUN chmod +x /container-server/startup.sh + +# Required for local development +EXPOSE 8080 +``` + +:::note +Port exposure in Dockerfile is only required for local development. In production, all ports are automatically accessible. +::: + +## Related resources + +- [Ports API reference](/sandbox/api/ports/) - Complete API documentation +- [Preview URLs concept](/sandbox/concepts/preview-urls/) - How preview URLs work +- [Background processes guide](/sandbox/guides/background-processes/) - Managing long-running services diff --git a/src/content/docs/sandbox/index.mdx b/src/content/docs/sandbox/index.mdx index 6d0545a0a4e190d..b573099a710bb94 100644 --- a/src/content/docs/sandbox/index.mdx +++ b/src/content/docs/sandbox/index.mdx @@ -114,55 +114,23 @@ df['sales'].sum() # Last expression is automatically returned ```typescript - import { getSandbox, connect, type Sandbox } from "@cloudflare/sandbox"; - -export { Sandbox } from "@cloudflare/sandbox"; - -type Env = { -Sandbox: DurableObjectNamespace; -}; - -const initialized = new Set(); - -export default { - async fetch(request: Request, env: Env): Promise { - const sandboxId = "my-sandbox"; - const sandbox = getSandbox(env.Sandbox, sandboxId); - - if (request.headers.get('Upgrade')?.toLowerCase() === 'websocket') { - // Initialize WebSocket server on first connection - if (!initialized.has(sandboxId)) { - const serverCode = ` - -Bun.serve({ -port: 8080, -fetch(req, server) { -if (server.upgrade(req)) return; -return new Response('WebSocket server', { status: 200 }); -}, -websocket: { -message(ws, msg) { -ws.send(\`Echo: \${msg}\`); -} -} -}); -`; -await sandbox.writeFile('/workspace/server.js', serverCode); -await sandbox.startProcess('bun /workspace/server.js'); -await new Promise(resolve => setTimeout(resolve, 1000)); -initialized.add(sandboxId); -} - - return await connect(sandbox, request, 8080); - } - - return new Response('WebSocket endpoint. Connect using ws:// protocol.', { status: 200 }); - -}, -}; - -``` - + import { getSandbox } from '@cloudflare/sandbox'; + + export default { + async fetch(request: Request, env: Env): Promise { + // Connect to WebSocket services in sandbox + if (request.headers.get('Upgrade')?.toLowerCase() === 'websocket') { + const sandbox = getSandbox(env.Sandbox, 'user-123'); + return await sandbox.wsConnect(request, 8080); + } + + return Response.json({ message: 'WebSocket endpoint' }); + } + }; + ``` + + Connect to WebSocket servers running in sandboxes. Learn more: [WebSocket Connections](/sandbox/guides/websocket-connections/). +