diff --git a/src/content/docs/sandbox/api/ports.mdx b/src/content/docs/sandbox/api/ports.mdx index 73a1aeac27c657..5c98b5bdd67b0a 100644 --- a/src/content/docs/sandbox/api/ports.mdx +++ b/src/content/docs/sandbox/api/ports.mdx @@ -24,6 +24,7 @@ const response = await sandbox.exposePort(port: number, options?: ExposePortOpti ``` **Parameters**: + - `port` - Port number to expose (1024-65535) - `options` (optional): - `name` - Friendly name for the port @@ -56,10 +57,11 @@ await sandbox.unexposePort(port: number): Promise ``` **Parameters**: + - `port` - Port number to unexpose -``` +```ts await sandbox.unexposePort(8000); ``` @@ -79,11 +81,49 @@ const response = await sandbox.getExposedPorts(): Promise +### `wsConnect()` + +Connect to WebSocket servers running in the sandbox. Use this when your Worker needs to establish WebSocket connections with services in the 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. + +```ts +const response = await sandbox.wsConnect(request: Request, port: number): Promise +``` + +**Parameters**: + +- `request` - Incoming WebSocket upgrade request +- `port` - Port number (1024-65535, excluding 3000) + +**Returns**: `Promise` - WebSocket response establishing the connection + + +```ts +import { getSandbox } from "@cloudflare/sandbox"; + +export default { + async fetch(request: Request, env: Env): Promise { + if (request.headers.get('Upgrade')?.toLowerCase() === 'websocket') { + const sandbox = getSandbox(env.Sandbox, 'my-sandbox'); + return await sandbox.wsConnect(request, 8080); + } + + return new Response('WebSocket endpoint', { status: 200 }); + } +}; +``` + + ## Related resources - [Preview URLs concept](/sandbox/concepts/preview-urls/) - How preview URLs work diff --git a/src/content/docs/sandbox/concepts/preview-urls.mdx b/src/content/docs/sandbox/concepts/preview-urls.mdx index c6419f084f1132..8ba00fadf8da3c 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 @@ -79,10 +80,31 @@ const admin = await sandbox.exposePort(3001, { name: "admin" }); - Raw TCP/UDP connections - Custom protocols (must wrap in HTTP) -- WebSocket connections - 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 00000000000000..ee4964a716a465 --- /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 d70a31282ec511..b573099a710bb9 100644 --- a/src/content/docs/sandbox/index.mdx +++ b/src/content/docs/sandbox/index.mdx @@ -112,6 +112,25 @@ df['sales'].sum() # Last expression is automatically returned }; ``` + + ```typescript + 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/). + @@ -269,3 +288,4 @@ Stateful coordination layer that enables Sandbox to maintain persistent environm +```