The proxyToSandbox() function handles routing for preview URLs, enabling secure access to services running inside sandboxes. It extracts routing information from subdomain patterns and proxies requests to the appropriate sandbox port.
Signature
async function proxyToSandbox<T extends Sandbox<any>, E extends SandboxEnv<T>>(
request: Request,
env: E
): Promise<Response | null>
Parameters
Incoming HTTP request with a URL matching the preview URL pattern: {port}-{sandboxId}-{token}.{domain}
Environment bindings containing the Sandbox Durable Object namespace. Must have a Sandbox property of type DurableObjectNamespace<T>.
Return value
Returns:
Response - If the request matches a preview URL pattern and was successfully routed to the sandbox.
null - If the request does not match a preview URL pattern (not a sandbox request).
Preview URLs use this subdomain pattern:
{port}-{sandboxId}-{token}.yourdomain.com
Components:
port - Port number (4-5 digits, 1024-65535, excluding 3000)
sandboxId - Sandbox identifier (lowercase, normalized)
token - Security token for authentication (lowercase alphanumeric + underscores)
Example:
8080-my-project-abc123xyz.example.com
Preview URLs require a custom domain with wildcard DNS (*.yourdomain.com). The default .workers.dev domain does not support the subdomain patterns needed for preview URLs.
Security
The function validates security tokens before proxying requests:
- Control plane (port 3000): No token validation (internal API)
- User ports (1024-65535): Token validation required
- Invalid tokens: Returns 404 with error message
Tokens are generated by the sandbox when exposing a port and must match for access.
Usage examples
Basic Worker setup
import { proxyToSandbox } from '@cloudflare/sandbox';
export default {
async fetch(request: Request, env: Env): Promise<Response> {
// Try to route as preview URL
const proxyResponse = await proxyToSandbox(request, env);
if (proxyResponse) {
return proxyResponse;
}
// Not a preview URL, handle as regular request
return new Response('Not Found', { status: 404 });
}
};
Combined with application routes
import { proxyToSandbox } from '@cloudflare/sandbox';
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
// Check for preview URL first
const proxyResponse = await proxyToSandbox(request, env);
if (proxyResponse) {
return proxyResponse;
}
// Handle application routes
if (url.pathname.startsWith('/api/')) {
return handleApiRequest(request, env);
}
// Default route
return new Response('Welcome', { status: 200 });
}
};
async function handleApiRequest(request: Request, env: Env): Promise<Response> {
// Your API logic
return new Response('API response');
}
With path-based routing
import { proxyToSandbox } from '@cloudflare/sandbox';
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
// Preview URLs have priority
const proxyResponse = await proxyToSandbox(request, env);
if (proxyResponse) {
return proxyResponse;
}
// Route based on path
switch (url.pathname) {
case '/':
return new Response('Home');
case '/health':
return new Response('OK');
default:
return new Response('Not Found', { status: 404 });
}
}
};
Logging and monitoring
import { proxyToSandbox } from '@cloudflare/sandbox';
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const startTime = Date.now();
const url = new URL(request.url);
// Try preview URL routing
const proxyResponse = await proxyToSandbox(request, env);
if (proxyResponse) {
const duration = Date.now() - startTime;
console.log('Preview URL request:', {
url: url.toString(),
status: proxyResponse.status,
duration
});
return proxyResponse;
}
// Handle non-preview requests
return new Response('Not Found', { status: 404 });
}
};
Error handling
import { proxyToSandbox } from '@cloudflare/sandbox';
export default {
async fetch(request: Request, env: Env): Promise<Response> {
try {
// Attempt preview URL routing
const proxyResponse = await proxyToSandbox(request, env);
if (proxyResponse) {
return proxyResponse;
}
// Not a preview URL
return new Response('Not Found', { status: 404 });
} catch (error) {
console.error('Routing error:', error);
return new Response('Internal Server Error', { status: 500 });
}
}
};
WebSocket support
The proxy automatically handles WebSocket upgrades:
import { proxyToSandbox } from '@cloudflare/sandbox';
export default {
async fetch(request: Request, env: Env): Promise<Response> {
// Handles both HTTP and WebSocket requests
const proxyResponse = await proxyToSandbox(request, env);
if (proxyResponse) {
return proxyResponse; // WebSocket upgrade response if applicable
}
return new Response('Not Found', { status: 404 });
}
};
WebSocket requests are detected by checking for Upgrade: websocket header and routed appropriately.
Response types
Successful proxy
Returns the response from the sandbox service:
Response {
status: 200,
headers: { 'content-type': 'application/json', ... },
body: ...
}
Invalid token
Returns 404 with error details:
Response {
status: 404,
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
error: 'Access denied: Invalid token or port not exposed',
code: 'INVALID_TOKEN'
})
}
Not a preview URL
Returns null when the request doesn’t match the pattern:
const response = await proxyToSandbox(request, env);
if (response === null) {
// Request is not for a sandbox preview URL
// Handle as regular application request
}
Container errors
Returns 503 or 500 for container startup/runtime errors:
// Container provisioning
Response {
status: 503,
headers: { 'Retry-After': '10' },
body: 'Container is currently provisioning...'
}
// Startup in progress
Response {
status: 503,
headers: { 'Retry-After': '3' },
body: 'Container is starting...'
}
// Permanent failure
Response {
status: 500,
body: 'Failed to start container: ...'
}
The function extracts routing information from the hostname:
// Example URL: 8080-my-project-abc123.example.com/api/data?key=value
const routeInfo = {
port: 8080, // Extracted from subdomain
sandboxId: 'my-project', // Normalized to lowercase
token: 'abc123', // Security token
path: '/api/data?key=value' // Preserved from original request
};
Parsing rules:
- Port: First segment before first hyphen (4-5 digits)
- Token: Last segment after last hyphen (alphanumeric + underscore)
- Sandbox ID: Everything between port and token (supports hyphens)
Request forwarding
The proxy preserves request properties:
// Original request headers are forwarded with additions:
{
'X-Original-URL': 'https://8080-my-project-abc123.example.com/path',
'X-Forwarded-Host': '8080-my-project-abc123.example.com',
'X-Forwarded-Proto': 'https',
'X-Sandbox-Name': 'my-project',
...originalHeaders
}
These headers help sandbox applications understand the original request context.
Production requirements
Custom domain setup
-
Add custom domain to your Cloudflare Workers:
wrangler publish --custom-domain=yourdomain.com
-
Configure wildcard DNS in Cloudflare:
*.yourdomain.com -> CNAME -> workers.cloudflare.com
-
Enable SSL certificate for wildcard domain.
Preview URLs do not work with .workers.dev domains. You must use a custom domain with wildcard DNS.
Security considerations
- Token validation: Tokens are mandatory for all user ports. Generate strong tokens when exposing ports.
- Port restrictions: Only ports 1024-65535 are allowed (excluding 3000, the control plane).
- Sandbox isolation: Each sandbox runs in an isolated container with its own filesystem and process space.
- Network isolation: Sandboxes can only expose specific ports; they cannot access other sandboxes.
Best practices
Place at route entry point
Always check for preview URLs first in your Worker:
export default {
async fetch(request: Request, env: Env): Promise<Response> {
// Check preview URLs FIRST
const proxyResponse = await proxyToSandbox(request, env);
if (proxyResponse) return proxyResponse;
// Then handle application routes
return handleApplicationRequest(request, env);
}
};
Handle null responses
Always check for null return value:
const proxyResponse = await proxyToSandbox(request, env);
if (proxyResponse !== null) {
return proxyResponse;
}
// Continue with application routing
Log security events
Monitor token validation failures:
const proxyResponse = await proxyToSandbox(request, env);
if (proxyResponse?.status === 404) {
const body = await proxyResponse.text();
if (body.includes('INVALID_TOKEN')) {
console.warn('Invalid token attempt:', {
url: request.url,
ip: request.headers.get('CF-Connecting-IP')
});
}
}
return proxyResponse;
Implement rate limiting
Protect preview URLs from abuse:
import { proxyToSandbox } from '@cloudflare/sandbox';
export default {
async fetch(request: Request, env: Env): Promise<Response> {
// Check rate limit for preview URLs
const url = new URL(request.url);
if (url.hostname.includes('-')) { // Likely a preview URL
const limited = await checkRateLimit(request, env);
if (limited) {
return new Response('Too Many Requests', { status: 429 });
}
}
const proxyResponse = await proxyToSandbox(request, env);
if (proxyResponse) return proxyResponse;
return new Response('Not Found', { status: 404 });
}
};
SandboxEnv
Environment type for Workers using sandboxes:
interface SandboxEnv<T extends Sandbox<any> = Sandbox<any>> {
Sandbox: DurableObjectNamespace<T>;
}
RouteInfo
Internal routing information extracted from preview URLs:
interface RouteInfo {
port: number; // Target port number
sandboxId: string; // Sandbox identifier (normalized)
path: string; // Request path
token: string; // Security token
}
See also