Sandbox: production preview URL setup guide for subdomains, request flow, and troubleshooting#28382
Sandbox: production preview URL setup guide for subdomains, request flow, and troubleshooting#28382ask-bonk[bot] wants to merge 7 commits intoproductionfrom
Conversation
Co-authored-by: irvinebroque <irvinebroque@users.noreply.github.com>
|
This pull request requires reviews from CODEOWNERS as it changes files that match the following patterns:
|
|
/bonk please remove the part about ACM pricing. Omit the specific error text. Please research the correct API for previews |
| - **SSL/TLS errors**: Wait a few minutes for certificate provisioning. Verify the DNS record is proxied and SSL/TLS mode is set to "Full" or "Full (strict)" in your dashboard. If your worker is on a subdomain (for example, `sandbox.yourdomain.com`), Universal SSL will not cover the second-level wildcard `*.sandbox.yourdomain.com` -- see the [TLS caution](#subdomain-depth-matters-for-tls) at the top of this page for options. | ||
| - **Preview URL not resolving**: Confirm the wildcard DNS record exists and is proxied. Wait 30-60 seconds for DNS propagation. | ||
| - **Port not accessible**: Ensure your service binds to `0.0.0.0` (not `localhost`) and that `proxyToSandbox()` is called first in your Worker's fetch handler. | ||
| - **"Connecting to a container using HTTPS is not currently supported"**: Your service inside the sandbox is using HTTPS (for example, Node.js `https.createServer()` or a framework with `--https` enabled). Configure it to serve plain HTTP instead. Cloudflare terminates TLS at the edge, so the browser connects over HTTPS, but the internal connection to your container uses HTTP. This is the same model as running behind any reverse proxy. If your framework checks the protocol and redirects HTTP to HTTPS, configure it to trust the proxy (for example, `app.set('trust proxy', 1)` in Express). |
There was a problem hiding this comment.
/bonk please review this line. Think we can frame it better than this — traffic from the Worker to the Container is already encrypted and secure. This needs to just point out that you dont need to use HTTPS yourself, that securing communication between worker and container is already handled by Cloudflare
|
Here is a summary of all changes made: Changes applied (2 files, 3 categories)1. Removed ACM pricing ($10/month) — 3 occurrences across 2 files
2. Omitted specific error text — 2 occurrences across 2 files
3. Fixed
Research finding on the API: The |
| - **Port not accessible**: Ensure your service binds to `0.0.0.0` (not `localhost`) and that `proxyToSandbox()` is called first in your Worker's fetch handler. | ||
| - **"Connecting to a container using HTTPS is not currently supported"**: Your service inside the sandbox is using HTTPS (for example, Node.js `https.createServer()` or a framework with `--https` enabled). Configure it to serve plain HTTP instead. Cloudflare terminates TLS at the edge, so the browser connects over HTTPS, but the internal connection to your container uses HTTP. This is the same model as running behind any reverse proxy. If your framework checks the protocol and redirects HTTP to HTTPS, configure it to trust the proxy (for example, `app.set('trust proxy', 1)` in Express). | ||
| - **Error 1016 (Origin DNS error)**: Cloudflare cannot resolve the origin for the requested hostname. Verify your wildcard DNS record exists and is proxied (orange cloud). If your Worker runs on a subdomain like `sandbox.yourdomain.com`, the DNS record name must be `*.sandbox`, not `*`. Wait 30-60 seconds for propagation after creating the record. | ||
| - **Error 522 (Connection timed out)**: Cloudflare timed out reaching the origin. This often happens temporarily after creating or changing DNS records and routes. Wait 60 seconds and retry. If it persists, verify `proxyToSandbox()` is called first in your Worker's fetch handler -- if it is not handling the request, Cloudflare tries to reach the `192.0.2.0` origin, which times out. |
There was a problem hiding this comment.
/bonk please link to the relevant Cloudflare docs for these two errors, 522 and 1016. We shouldn't explain them in depth here. We should link to sources of truth. And we shouldn't say that this often happens when creating or changing records.
Co-authored-by: irvinebroque <irvinebroque@users.noreply.github.com>
|
Here's what I changed:
|
|
/bonk please make updates to this PR based on the feedback below: Sandbox docs PR #28382 — additional changesPR: #28382 The PR covers the DNS/TLS/routing issues well. The gap is in the Container HTTPS Error troubleshooting: it assumes the server itself is listening on HTTPS, when the more common failure (especially with Next.js) is the container producing HTTPS redirect URLs that workerd follows internally. Context: the redirect-following mechanismThe container binding ( This happens even when the server serves plain HTTP, because:
Changes to make1. Expand the "Container HTTPS Error" troubleshooting bullet in
|
Expand Container HTTPS Error troubleshooting in both production-deployment.mdx and preview-urls.mdx to cover the redirect-following mechanism where frameworks like Next.js construct https:// redirect URLs from X-Forwarded-Proto. Add debugging tip for redirect: manual, Next.js-specific subsection, and update the request flow steps to document X-Forwarded-Proto and redirect behavior.
|
All 5 changes have been applied and committed: Summary of changes
|
|
The comment There's no actionable code change request in this comment — it's a meta-complaint about the bot not triggering on a previous invocation. The diff context shows the line where contractions were removed ( Since there's no specific content change being requested here, there's nothing for me to do. The reviewer was just noting that a prior |
| 2. **TLS**: Cloudflare terminates TLS at the edge using your domain's certificate. The browser gets a secure HTTPS connection. | ||
| 3. **Worker route**: The wildcard route (`*.yourdomain.com/*`) matches the request and sends it to your Worker. | ||
| 4. **`proxyToSandbox()`**: Your Worker calls `proxyToSandbox()`, which extracts the sandbox ID and port from the hostname, rewrites the URL to `http://localhost:{port}`, and forwards the request to the correct Durable Object. It sets `X-Forwarded-Proto: https` so your application can detect the original protocol. | ||
| 5. **Container**: The Durable Object forwards the request to the service inside your container over an internal connection (HTTP -- TLS is not needed because the transport is already secure within Cloudflare's network). If your service responds with an HTTP redirect, the container binding follows it automatically. If the redirect `Location` header contains an `https://` URL, the request fails. To prevent this, ensure your application produces HTTP or relative redirect URLs, or pass `redirect: "manual"` in your Worker to handle redirects yourself. |
There was a problem hiding this comment.
/bonk can you create a mermaid diagram of this request flow based on the description in this section?
Please don't yet add it to the docs but just leave it here as a comment and put it in a markdown block to render
|
Here's the mermaid diagram for the preview URL request flow: ```mermaid
sequenceDiagram
participant Browser
participant DNS
participant Edge as Cloudflare Edge (TLS)
participant Route as Worker Route
participant Worker as Worker (proxyToSandbox)
participant DO as Durable Object
participant Container as Container (HTTP)
Browser->>DNS: Resolve 8080-my-sandbox-abc123.yourdomain.com
DNS-->>Browser: *.yourdomain.com → Cloudflare proxy IP
Browser->>Edge: HTTPS request
Edge->>Edge: Terminate TLS using domain certificate
Edge-->>Browser: Secure HTTPS connection established
Edge->>Route: Forward request
Route->>Route: Match *.yourdomain.com/*
Route->>Worker: Dispatch to Worker
Worker->>Worker: Extract sandbox ID & port from hostname
Worker->>Worker: Rewrite URL to http://localhost:{port}
Worker->>Worker: Set X-Forwarded-Proto: https
Worker->>DO: Forward request to Durable Object
DO->>Container: HTTP request (internal, already secure)
Container-->>DO: HTTP response
DO-->>Worker: Response
Worker-->>Edge: Response
Edge-->>Browser: HTTPS response
Note over DO,Container: If container responds with 302 Location: https://...<br/>the binding follows the redirect and fails.<br/>Use redirect: "manual" to debug.
```This covers all 5 steps from the section: DNS resolution → TLS termination at the edge → worker route matching → |
|
|
||
| #### Next.js | ||
|
|
||
| Next.js reads the `X-Forwarded-Proto` header and uses it to construct internal URLs. When running `next dev` behind a Sandbox preview URL: |
There was a problem hiding this comment.
/bonk please substantiate this claim by researching the Next.js codebase (https://github.com/vercel/next.js) and linking to relevant context
|
|
||
| To fix: | ||
|
|
||
| - Set your auth library's base URL to the container's internal address: `NEXTAUTH_URL=http://localhost:3001` or `AUTH_URL=http://localhost:3001` |
There was a problem hiding this comment.
/bonk this should be clear that you should do this only in the context of local dev within the sandbox, in a context where TLS is handled by the proxy in front
|
/bonk please add the mermaid diagram you created on a previous comment to the docs, in the relevant section |
There was a problem hiding this comment.
Research: Next.js X-Forwarded-Proto handling — source code substantiation
I investigated the Next.js codebase (vercel/next.js, canary branch) to substantiate each claim in this section. All claims are confirmed. Here are the relevant source locations:
Claim 1: "Next.js reads the X-Forwarded-Proto header and uses it to construct internal URLs"
packages/next/src/server/lib/router-utils/resolve-routes.ts (source):
const protocol =
(req?.socket as TLSSocket)?.encrypted ||
req.headers['x-forwarded-proto']?.includes('https')
? 'https'
: 'http'
const initUrl = opts.port
? `${protocol}://${formatHostname(opts.hostname || 'localhost')}:${opts.port}${req.url}`
: req.url || ''
addRequestMeta(req, 'initURL', initUrl)
addRequestMeta(req, 'initProtocol', protocol)This is the primary location. The protocol is derived from x-forwarded-proto and stored as initProtocol in request metadata, then used to construct initUrl.
packages/next/src/server/base-server.ts (source) also reads it:
const xForwardedProto = originalRequest?.headers['x-forwarded-proto']
const isHttps = xForwardedProto
? xForwardedProto === 'https'
: !!(originalRequest?.socket as TLSSocket)?.encrypted
req.headers['x-forwarded-proto'] ??= isHttps ? 'https' : 'http'Claim 2: "Middleware receives request.url with https:// protocol"
packages/next/src/server/next-server.ts — runMiddleware method (source):
url = `${getRequestMeta(params.request, 'initProtocol')}://${
this.fetchHostname || 'localhost'
}:${this.port}${locale ? `/${locale}` : ''}${params.parsed.pathname}${
query ? `?${query}` : ''
}`The middleware request URL is constructed using initProtocol (from x-forwarded-proto). If a proxy set X-Forwarded-Proto: https, middleware's request.url will have an https:// scheme.
Claim 3: "Server Actions that call redirect() trigger an internal self-fetch using the HTTPS protocol"
packages/next/src/server/app-render/action-handler.ts — createRedirectRenderResult (source):
const proto =
getRequestMeta(req, 'initProtocol')?.replace(/:+$/, '') || 'https'
const origin =
process.env.__NEXT_PRIVATE_ORIGIN || `${proto}://${originalHost.value}`
const fetchUrl = new URL(
`${origin}${appRelativeRedirectUrl.pathname}${appRelativeRedirectUrl.search}`
)
const response = await fetch(fetchUrl, { method: 'GET', headers: forwardedHeaders, ... })When a Server Action calls redirect(), Next.js performs a self-fetch to the redirect target to stream an RSC response. The fetch URL uses initProtocol (derived from x-forwarded-proto) with a fallback to 'https'. If the proxy set X-Forwarded-Proto: https, this internal fetch goes to https://host/..., which fails if the server only listens on HTTP.
The same pattern exists in createForwardedActionResponse in the same file.
Claim 4: __NEXT_PRIVATE_ORIGIN controls the origin for internal requests
packages/next/src/server/lib/start-server.ts (source):
const protocol = selfSignedCertificate ? 'https' : 'http'
const appUrl = `${protocol}://${formattedHostname}:${port}`
process.env.__NEXT_PRIVATE_ORIGIN = appUrlNext.js sets __NEXT_PRIVATE_ORIGIN at startup to the actual listen address. In action-handler.ts, __NEXT_PRIVATE_ORIGIN takes precedence over the initProtocol-derived origin:
const origin = process.env.__NEXT_PRIVATE_ORIGIN || `${proto}://${originalHost.value}`Setting __NEXT_PRIVATE_ORIGIN=http://localhost:3001 overrides the HTTPS protocol for internal self-fetches, which is the recommended fix for running behind a reverse proxy like Sandbox.
The chain of events in a Sandbox context
- Browser request arrives at Cloudflare edge over HTTPS
proxyToSandbox()forwards to the container, settingX-Forwarded-Proto: https- Next.js reads this header in
resolve-routes.ts→initProtocol = 'https' - Middleware receives
request.urlwithhttps://scheme (fromnext-server.ts) - Server Action
redirect()constructs a self-fetch URL usinghttps://(fromaction-handler.ts) - The container binding follows this redirect internally and rejects the
https://scheme - Fix: Set
__NEXT_PRIVATE_ORIGIN=http://localhost:3001so internal fetches use HTTP
All claims in the docs section are substantiated by the Next.js source code.
|
All claims in the Next.js section at line 257 are substantiated by the Next.js source code. I posted a detailed review comment on the PR with links to four key files in
The documentation's recommendation to set |
|
/bonk please take your findings about Next.js and the links, and add links where relevant to the documentation where we call out this Next.js behavior. This should help the reader understand where in Next.js this behavior happens |
|
Added the mermaid sequence diagram to the "How preview URL requests flow" section in
It also includes a note about the redirect-following pitfall where |
|
/bonk please take your findings about Next.js and the links, and add links where relevant to the documentation where we call out this Next.js behavior. This should help the reader understand where in Next.js this behavior happens |
Co-authored-by: irvinebroque <irvinebroque@users.noreply.github.com>
|
The structure looks clean. The "Apex domain" and "Subdomain" examples are now under separate H4 headings ( Changes made to
|

All 10 changes from the issue have been implemented. Here is a summary of what was done and my review findings:
Changes implemented
src/content/docs/sandbox/guides/production-deployment.mdx*) vs. Subdomain (*.sandbox) with clear explanations of how preview URLs differ in each case.*.yourdomain.com/*) vs. Subdomain (*.sandbox.yourdomain.com/*).:::cautionblock directly in the subdomain DNS setup path so developers cannot miss it.src/content/docs/sandbox/concepts/preview-urls.mdxhttps.createServer, framework HTTPS flags, proxy trust).Items flagged for human review
Advanced Certificate Manager pricing ($10/month): This price appears only in the sandbox docs and nowhere in the official ACM documentation (
/ssl/edge-certificates/advanced-certificate-manager/). The ACM docs do not list any specific price. I preserved the$10/monthfigure since it was already in the existing page and the issue explicitly includes it, but this should be verified against current pricing and may go stale. Consider linking to a pricing page instead of hardcoding the amount.Error message text
"Connecting to a container using HTTPS is not currently supported": This exact error string comes from the issue author (who is a Cloudflare employee on the Sandbox team), so it is likely accurate. However, I could not verify this error string exists in any public documentation or source code in the docs repo. If the error message text changes in the SDK, the docs will need updating.exposed.exposedAtvsexposed.url: The Verify section in production-deployment.mdx usesexposed.exposedAtwhile the preview-urls.mdx page usesexposed.url. Both were already present in the existing docs before my changes, but this inconsistency may confuse developers. Worth checking which is the current API.Step 5 of the request flow ("Container: The Durable Object forwards the request to the service inside your container over an internal connection (HTTP)"): The claim that transport between the Durable Object and container is "already secure within Cloudflare's network" is architecturally reasonable but is a security claim I cannot independently verify from the docs. It was stated in the issue by a Cloudflare engineer, so it is likely accurate.
Closes #28381
github run