Skip to main content
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

request
Request
required
Incoming HTTP request with a URL matching the preview URL pattern: {port}-{sandboxId}-{token}.{domain}
env
E
required
Environment bindings containing the Sandbox Durable Object namespace. Must have a Sandbox property of type DurableObjectNamespace<T>.

Return value

response
Response | null
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 URL format

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: ...'
}

URL extraction

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

  1. Add custom domain to your Cloudflare Workers:
    wrangler publish --custom-domain=yourdomain.com
    
  2. Configure wildcard DNS in Cloudflare:
    *.yourdomain.com -> CNAME -> workers.cloudflare.com
    
  3. 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

Build docs developers (and LLMs) love