Skip to content

Implement an example project using vite to run both host and sandboxed applications#434

Merged
ghostwriternr merged 5 commits intomainfrom
example-vite-devserver
Mar 26, 2026
Merged

Implement an example project using vite to run both host and sandboxed applications#434
ghostwriternr merged 5 commits intomainfrom
example-vite-devserver

Conversation

@aron-cf
Copy link
Copy Markdown
Contributor

@aron-cf aron-cf commented Feb 27, 2026

This PR introduces an example project demonstrating a Vite React application embedded in a sandbox hosted by a Vite React application. A "counter" script changes the sandbox App.jsx file to demonstrate hot module reloading (HMR).

Screen.Recording.2026-03-16.at.20.22.42.mov

Setup

Start the development server:

cd examples/vite-sandbox
npm start

Usage

This is a non-interactive demo. The counters in both host and iframed sandbox will increment/decrement once per second to demonstrate that the hot module reloading is working over websockets between browser and sandbox.

Deploy

npm run deploy

Implementation Notes

Hosting two Vite servers on the same port along with the Cloudflare wrangler server has the potential for unexpected behavior.

We refer to the current directory as the "host" server and the one loaded in the sandbox as the "sandbox" server. The Cloudflare services (workers, assets, storage etc.) are referred to as wrangler. Configuration for the host Vite server is in the root vite.config.js, the Cloudflare config is in wrangler.jsonc and the sandbox Vite config is in sandbox-app/vite.config.js.

This repository has been setup in a way to reduce the confusion.

  1. We assume static assets will be served by Cloudflare. The host Vite server has appType set to "custom" to disable Vite handling HTML.
  2. The hot module reloading server is configured under server.hmr has been set to run on a different port to the Vite dev server. This reduces the chance of conflicts between the host and sandbox HMR websockets.
  3. We pass the host port via the VITE_CLIENT_PORT environment variable so that the HMR server is configured correctly.

@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Feb 27, 2026

⚠️ No Changeset found

Latest commit: 0365672

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

github-actions[bot]

This comment was marked as outdated.

@aron-cf
Copy link
Copy Markdown
Contributor Author

aron-cf commented Feb 27, 2026

I'm still debugging why this doesn't work. With a default setup we run into issues with the host dev server running with the @cloudflare/vite plugin intercepting the sandbox request for the @vite/client file.

e.g. for a dev server running on localhost:3000 with a sandbox server running on port 5173 you'd get a request to:

http://5173-vite-sandbox-<token>.localhost:3000/@vite/client

This would return the configuration for the host dev server, and the subsequent websocket would be:

ws://5173-vite-sandbox-<token>.localhost:3000/?token=fr4WLvivldEq

This would again connect to the host websocket rather than the sandbox one.

This can be worked around by using the base setting in the sandbox vite.config.js file to something like /sandbox. This then separates the two and the initial request becomes:

http://5173-vite-sandbox-<token>.localhost:3000/sandbox/@vite/client

This has the drawback that the sandbox server must be served from a prefixed path, but for now that's probably fine as a constraint. However for some reason this still doesn't work :/ The websocket connection no longer connects to anything. So needs more investigation.

In the container I can use websocat to connect to the stream just fine:

curl -L https://github.com/vi/websocat/releases/download/v1.14.1/websocat.aarch64-unknown-linux-musl > websocat
chmod +x websocat
 ./websocat "ws://0.0.0.0:5173/sandbox/?token=fr4WLvivldEq" --protocol vite-hmr

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Feb 27, 2026

Open in StackBlitz

npm i https://pkg.pr.new/cloudflare/sandbox-sdk/@cloudflare/sandbox@434

commit: 0365672

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Feb 27, 2026

🐳 Docker Images Published

Default:

FROM cloudflare/sandbox:0.0.0-pr-434-f531f6d

With Python:

FROM cloudflare/sandbox:0.0.0-pr-434-f531f6d-python

With OpenCode:

FROM cloudflare/sandbox:0.0.0-pr-434-f531f6d-opencode

Version: 0.0.0-pr-434-f531f6d

Use the -python variant if you need Python code execution, or -opencode for the variant with OpenCode AI coding agent pre-installed.


📦 Standalone Binary

For arbitrary Dockerfiles:

COPY --from=cloudflare/sandbox:0.0.0-pr-434-f531f6d /container-server/sandbox /sandbox
ENTRYPOINT ["/sandbox"]

Download via GitHub CLI:

gh run download 23014149621 -n sandbox-binary

Extract from Docker:

docker run --rm cloudflare/sandbox:0.0.0-pr-434-f531f6d cat /container-server/sandbox > sandbox && chmod +x sandbox

Copy link
Copy Markdown
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OpenCode Review

This PR adds a new Vite development server example for the Cloudflare Sandbox SDK. The example demonstrates running a Vite dev server inside a sandbox container and exposing it via preview URLs with Hot Module Replacement (HMR) support.

Missing Critical Configuration: The example is missing a wrangler.jsonc configuration file. All other examples in the repository have either wrangler.toml or wrangler.jsonc files that define the Durable Object bindings, container configuration, and other Worker settings. Without this file, the example won't work.

Code Issues:

  1. Hard-coded token in debug code - WebSocket connection uses a hard-coded token that should be dynamic or documented as debug code
  2. Unused debug code - The /debug endpoint appears to contain development/debugging code that should be cleaned up
  3. Docker version mismatch - Uses cloudflare/sandbox:0.7.6 but should match current SDK version for consistency

Missing Documentation: The example would benefit from a README.md explaining how HMR works with sandboxes and the specific setup required for Vite integration.

The implementation correctly follows SDK patterns for port exposure and process management, but needs the wrangler configuration to function.

if (url.pathname === '/debug') {
console.log(
'connecting to',
`${baseURL.replace('http', 'ws')}/?token=fr4WLvivldEq`
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hard-coded token fr4WLvivldEq in WebSocket connection. Should this be dynamically generated or is this debug code?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hard-coded tokens and debug endpoints should still be addressed. These aren't suitable for production examples that developers might copy.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These hardcoded values and debug endpoints are still present. Should they be removed or properly documented as development-only code?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hardcoded values and debug endpoints are still present in the code. These should be removed or properly documented as development-only code before this example is ready for production use.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hardcoded values remain in the code. This example should demonstrate production-ready patterns, not development debugging code.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These hardcoded values remain after the history rewrite. For a production-ready example, consider making the sandbox ID dynamic (e.g., session-based) and moving hardcoded values to configuration.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These hardcoded values remain after the history rewrite. For a production-ready example, consider making the sandbox ID dynamic (e.g., session-based) and moving hardcoded values to configuration.


const baseURL = `${port.url}sandbox`;
console.log('baseURL', baseURL);
if (url.pathname === '/debug') {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This /debug endpoint contains development/testing code with hard-coded values and a 10-second delay. Should this be removed or documented as debug functionality?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The /debug endpoint with hard-coded delays is still present - this should be removed or documented as development-only code.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The debug endpoint with hardcoded delays remains. This needs to be addressed for a production-ready example.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The debug endpoint with hardcoded delays remains in the code. This needs to be addressed for a production-ready example.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The debug endpoint with hardcoded delays remains. This should be removed or clearly documented as debug-only functionality.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The debug endpoint and hardcoded delays are still present. Since this is meant to be a reference example, these debug artifacts should be removed or clearly documented as development-only code.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The debug endpoint and hardcoded delays are still present. Since this is meant to be a reference example, these debug artifacts should be removed or clearly documented as development-only code.

@@ -0,0 +1,9 @@
FROM docker.io/cloudflare/sandbox:0.7.6
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Docker image version 0.7.6 should match current SDK version. Consider using a wildcard or updating to match other examples for consistency.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is still an issue - using version 0.7.6 instead of matching the current SDK version creates inconsistency.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, after checking the current SDK version in packages/sandbox/package.json, version 0.7.6 is correct. The Dockerfile version matches the SDK version exactly. This is resolved.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, after checking the current SDK version in packages/sandbox/package.json, version 0.7.6 is correct. The Dockerfile version matches the SDK version exactly. This issue is resolved.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Version 0.7.6 is correct and matches the current SDK version. This issue is resolved.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Version 0.7.6 is correct and matches the current SDK version. This issue is resolved.

@aron-cf aron-cf force-pushed the example-vite-devserver branch from 2ec4216 to 1037f08 Compare March 1, 2026 22:29
Copy link
Copy Markdown
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OpenCode Review

The hard-coded tokens and debug endpoints from the previous version have been cleaned up, which is great! However, there are still a few issues to address:

Docker version mismatch: The Dockerfile still uses version 0.7.6 instead of matching the current SDK version. This creates inconsistency and could lead to compatibility issues.

Implementation looks solid: The core pattern is good - checking for existing exposed ports, starting the Vite dev server, waiting for it to come up, then using HTMLRewriter to inject the correct base href. The counter background process is a nice demo touch.

Console.log statements: There are debug console.log statements that should be removed or converted to proper logging.

A couple minor improvements to consider for better error handling and consistency with other examples.

});
}

const baseURL = `${port.url}sandbox`;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Debug console.log statement should be removed before merge.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Debug console.log statements remain - these should be cleaned up for a polished example.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Debug console.log statements are still present in vite.config.js (lines 26, 60). These should be removed.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Debug console.log statements are still present throughout multiple files. These need cleanup.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Debug console statements are still scattered throughout the codebase and need cleanup before this can serve as a clean production reference.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Debug console statements are still scattered throughout the codebase and need cleanup before this can serve as a clean production reference.

if (!response) {
return new Response('Unexpected error', { status: 500 });
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Debug console.log statement should be removed before merge.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another debug console.log that should be removed.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional debug console.log to clean up.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional debug console.log statements remain.

Copy link
Copy Markdown
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OpenCode Review

The Docker image version issue is resolved - 0.7.6 is correct and matches the current SDK version. However, several debug artifacts remain that need attention before merge.

The example demonstrates a solid pattern for running Vite dev servers in sandboxes with HMR support. The architecture is clean: host Worker serves the UI while the sandbox runs the actual Vite server, connected via preview URLs.

A few things to address:

  1. Biome formatter enabled: The change from "enabled": false to "enabled": true affects the entire repo's formatting. This should be a separate decision/PR, not bundled with this example.

  2. Debug artifacts: Still seeing console.error statements and debug-focused code that should be cleaned up.

  3. Example completeness: The counter.js demonstration is clever but the overall example would benefit from documentation explaining the HMR setup and why the architecture works this way.

Overall architecture is sound, just needs cleanup of non-essential changes and debug code.

biome.json Outdated
},
"formatter": {
"enabled": false,
"enabled": true,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Enabling the formatter for the entire repository should be a separate decision/PR. This change affects all files and shouldn't be bundled with this example.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The global Biome formatter change still needs to be reverted. This affects the entire repository and should be a separate decision.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The global Biome formatter change still needs to be reverted - this affects the entire repository and should be a separate decision.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The global Biome formatter change still needs to be reverted - this affects the entire repository and should be a separate decision.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This global Biome formatter change still needs to be reverted - it affects the entire repository and should be a separate decision.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The global Biome formatter change is still present and should be reverted. This repository-wide change should be handled in a separate PR focused specifically on that decision.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The global Biome formatter change is still present and should be reverted. This repository-wide change should be handled in a separate PR focused specifically on that decision.

fetch("/api/sandbox")
.then((r) => r.json())
.then((data) => setSandboxUrl(data.url))
.catch((err) => console.error(err));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Debug console.error should be removed or replaced with proper error handling before merge.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Debug console.error in App.jsx line 10 should be replaced with proper error handling.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Debug console.error in App.jsx should still be replaced with proper error handling.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Debug console.error statements should be replaced with proper error handling.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Debug console.error statements should be replaced with proper error handling for a production-ready example.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Debug console.error statements should be replaced with proper error handling for a production-ready example.

github-actions[bot]

This comment was marked as outdated.

@aron-cf aron-cf changed the title example vite devserver Implement an example project using vite to run both host and sandboxed applications Mar 4, 2026
Copy link
Copy Markdown
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OpenCode Review

The Docker version issue is resolved (0.7.6 correctly matches the SDK version). However, several development/debug issues remain:

Still to address:

  • Debug console.log statements in vite.config.js (lines 26, 60) should be removed for production examples
  • Global Biome formatter change affects the entire repository - this should be reverted and handled separately
  • Hard-coded tokens/debug endpoints mentioned in previous feedback need to be addressed

New observations:

  • The example demonstrates proper Cloudflare Containers + Vite integration using preview URLs
  • Worker implementation correctly uses proxyToSandbox() pattern
  • Port exposure follows documented best practices
  • HMR WebSocket handling is well-architected

Architecture assessment: The implementation properly showcases the three-layer architecture with host Vite server, Worker proxy layer, and sandbox container running another Vite dev server. The preview URL pattern and WebSocket proxying demonstrate advanced Cloudflare Containers usage.

Revert the global Biome change and clean up debug statements, then this will be ready.


// Register middleware before default middleware.
server.middlewares.use(async (req, res, next) => {
console.log("request", req.url);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Debug console.log should be removed from production example.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Debug console.log should be removed.

const headers = createHeaders(request);
headers.set(CoreHeaders.ROUTE_OVERRIDE, entryWorkerName);

console.log("upgrade in flight", url);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another debug console.log to remove.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another debug console.log to remove.

@aron-cf aron-cf marked this pull request as ready for review March 4, 2026 11:43
github-actions[bot]

This comment was marked as outdated.

github-actions[bot]

This comment was marked as outdated.

Copy link
Copy Markdown
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OpenCode Review

Looks like history was rewritten since my last review, so starting fresh with the current state.

This is an impressive technical demo showing Vite HMR tunneling through sandbox boundaries. The architecture is sound and demonstrates advanced SDK usage patterns. However, there are some issues that need addressing:

Major Issues:

  1. Multi-tenancy problem: All users share the same sandbox ID 'vite-sandbox' - this won't work for multiple concurrent users
  2. Global Biome formatter change: Enabling the formatter repository-wide shouldn't be part of this PR
  3. Unstable dependency: Using a PR preview package instead of stable version

Minor Issues:

  • Debug console statements throughout the codebase need cleanup
  • Hard-coded configuration values that should be configurable

What's Good:

  • Excellent documentation and clear separation of host/sandbox concerns
  • Proper SDK usage patterns that other developers can learn from
  • Creative solution for complex WebSocket proxying scenario

Verdict: The core concept is solid but needs cleanup before merge. The multi-tenancy issue is the biggest blocker - consider using session-based sandbox IDs.

};

async function handleAPISandboxRoute(url, env) {
const sandbox = getSandbox(env.Sandbox, 'vite-sandbox');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Multi-tenancy issue: All users will share the same sandbox instance. Consider using session-based IDs like getSandbox(env.Sandbox, vite-${request.headers.get('CF-Ray') || Date.now()})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This multi-tenancy issue remains the most critical concern - all users sharing the same sandbox instance isn't acceptable for a production reference example.

"author": "",
"license": "MIT",
"dependencies": {
"@cloudflare/vite-plugin": "https://pkg.pr.new/@cloudflare/vite-plugin@12794"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using a PR preview package creates fragility. This should use a stable npm version once the vite-plugin changes are released.

devin-ai-integration[bot]

This comment was marked as resolved.

github-actions[bot]

This comment was marked as outdated.

devin-ai-integration[bot]

This comment was marked as resolved.

@aron-cf aron-cf force-pushed the example-vite-devserver branch from ab2340b to 0d6bdfb Compare March 16, 2026 10:58
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

View 17 additional findings in Devin Review.

Open in Devin Review

Comment on lines +143 to +144
const escapedPath = shellEscape(path);
const mimeResult = await exec(`file --mime-type -b ${escapedPath}`);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MIME type shell fallback uses original (potentially relative) path instead of resolved absolute path

In the read() method, after resolving a relative path to an absolute path via resolvePathInSession(), the MIME type shell fallback command still uses the original path variable (shellEscape(path)) instead of absolutePath. While Bun.file(absolutePath) correctly opens the resolved file, the file --mime-type -b shell command runs with the unresolved path. Although the session shell's CWD may resolve the relative path the same way, this is fragile: the resolvePathInSession call and the file command are separate shell executions, and if the session's CWD were to differ (e.g., due to a prior cd in a scoped env block), the shell command would detect the MIME type of a different file or fail entirely. The getFileMetadata() method at packages/sandbox-container/src/services/file-service.ts:991 has the same pattern but receives absolutePath from its caller (readFileStreamOperation at line 1388), so it is not affected in practice.

Suggested change
const escapedPath = shellEscape(path);
const mimeResult = await exec(`file --mime-type -b ${escapedPath}`);
const escapedPath = shellEscape(absolutePath);
const mimeResult = await exec(`file --mime-type -b ${escapedPath}`);
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@aron-cf aron-cf force-pushed the example-vite-devserver branch from 0d6bdfb to adf2b49 Compare March 16, 2026 20:25
@aron-cf aron-cf changed the base branch from example-websocket-tunnel to main March 16, 2026 20:27
@aron-cf aron-cf requested a review from scuffi as a code owner March 16, 2026 20:27
@aron-cf aron-cf force-pushed the example-vite-devserver branch from adf2b49 to 9f14ee5 Compare March 16, 2026 20:29
devin-ai-integration[bot]

This comment was marked as resolved.

whoiskatrin
whoiskatrin previously approved these changes Mar 16, 2026
devin-ai-integration[bot]

This comment was marked as resolved.

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

View 25 additional findings in Devin Review.

Open in Devin Review

Comment on lines +28 to +45
let port = await sandbox
.getExposedPorts(url.host)
.then((ports) => ports.find((p) => p.port === VITE_PORT));

if (!port) {
port = await sandbox.exposePort(VITE_PORT, { hostname: url.host });

const process = await sandbox.startProcess('npm run dev', {
processId: 'vite-dev-server',
cwd: '/app',
env: {
VITE_BASE: VITE_BASE,
VITE_PORT: `${VITE_PORT}`,
VITE_HMR_CLIENT_PORT: url.port ? `${url.port}` : undefined
}
});
await process.waitForPort(VITE_PORT);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Exposed port without running server creates permanently broken state on startup failure

In handleAPISandboxRoute, exposePort is called before startProcess and waitForPort. If either startProcess or waitForPort throws (e.g., npm run dev fails, timeout, etc.), the port remains exposed in the Durable Object but no Vite server is running behind it. All subsequent requests to /api/sandbox will find the port via getExposedPorts at line 29, skip the entire startup block, and return a non-functional URL. This stuck state persists until the sandbox sleeps and the container restarts.

Reproduction scenario
  1. First request hits /api/sandbox
  2. exposePort(5173) succeeds (line 33)
  3. startProcess('npm run dev', ...) or waitForPort(5173) throws
  4. Error propagates to caller
  5. Second request hits /api/sandbox
  6. getExposedPorts finds port 5173 already exposed (line 29-30)
  7. Startup block is skipped
  8. Returns URL pointing to port with no server behind it
Prompt for agents
In examples/vite-sandbox/src/worker.js, the handleAPISandboxRoute function (lines 25-48) needs to be restructured so that the exposed port alone is not used as a signal that the Vite server is running. Two possible approaches:

1. Move exposePort after startProcess + waitForPort, so the port is only exposed once the server is confirmed running. Change lines 32-45 to: first startProcess, then waitForPort, then exposePort.

2. Alternatively, add error handling: wrap lines 35-44 (startProcess + waitForPort) in a try/catch that calls sandbox.unexposePort(VITE_PORT) if startup fails, so the next request retries.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

aron-cf and others added 3 commits March 24, 2026 22:23
Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The vite-sandbox example used older versions of react, vite, and
wrangler than the rest of the workspace. Also bumps
@cloudflare/vite-plugin to ^1.30.1 across all packages.
@ghostwriternr ghostwriternr force-pushed the example-vite-devserver branch from 0d0ec11 to 6298d83 Compare March 24, 2026 22:27
ghostwriternr
ghostwriternr previously approved these changes Mar 24, 2026
@ghostwriternr ghostwriternr enabled auto-merge (squash) March 24, 2026 22:27
@ghostwriternr
Copy link
Copy Markdown
Member

/bonk fix the merge conflict

Copy link
Copy Markdown
Contributor

@ask-bonk ask-bonk bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OpenCode Review

This example effectively demonstrates running a Vite dev server inside a Cloudflare Sandbox with HMR tunneled through preview URLs. The architecture is clean, the README is thorough, and it follows existing SDK patterns well. The previous iteration's issues (biome.json changes, debug endpoints, console.log statements, hard-coded tokens) have all been cleaned up -- nice work.

A few remaining issues to address:

Bugs

  1. Startup failure creates permanently broken state (src/worker.js:32-44): exposePort() is called before startProcess() and waitForPort(). If either throws, the port remains exposed but no server is running. All subsequent requests will find the exposed port via getExposedPorts() (line 29), skip the startup block, and return a non-functional URL. This persists until the sandbox sleeps. Fix by moving exposePort() after waitForPort(), or wrapping the startup in a try/catch that calls unexposePort() on failure.

  2. HMR broken in production (src/worker.js:41): When deployed on HTTPS (port 443), url.port returns "" per the URL spec. The falsy empty string causes VITE_HMR_CLIENT_PORT to be undefined, and sandbox-app/vite.config.js:28 defaults it to 3000. The HMR client in the browser then tries to connect on port 3000, which won't work. Should fall back to the protocol-appropriate port: url.port || (url.protocol === 'https:' ? '443' : '80').

  3. Malformed JSONC in README (README.md:58): The run_worker_first value "!/assets] is missing the closing quote and bracket. Should be "!/assets"].

Minor Issues

  1. Stray value.js in example root: examples/vite-sandbox/value.js appears to be a leftover -- the host counter uses src/value.js and the sandbox uses sandbox-app/src/value.js. This file isn't referenced anywhere.

  2. Missing newline at end of file: sandbox-app/index.html is missing a trailing newline.

.then((ports) => ports.find((p) => p.port === VITE_PORT));

if (!port) {
port = await sandbox.exposePort(VITE_PORT, { hostname: url.host });
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Startup failure creates permanently broken state. exposePort() is called before the server is confirmed running. If startProcess() or waitForPort() throws, the port stays exposed but has no server behind it. Subsequent requests find the exposed port at line 29, skip the startup block, and return a non-functional URL.

Fix by moving exposePort() after waitForPort():

if (!port) {
    const process = await sandbox.startProcess('npm run dev', {
      processId: 'vite-dev-server',
      cwd: '/app',
      env: {
        VITE_BASE: VITE_BASE,
        VITE_PORT: `${VITE_PORT}`,
        VITE_HMR_CLIENT_PORT: url.port ? `${url.port}` : undefined
      }
    });
    await process.waitForPort(VITE_PORT);
    port = await sandbox.exposePort(VITE_PORT, { hostname: url.host });
}

env: {
VITE_BASE: VITE_BASE,
VITE_PORT: `${VITE_PORT}`,
VITE_HMR_CLIENT_PORT: url.port ? `${url.port}` : undefined
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When deployed on HTTPS (port 443) or HTTP (port 80), url.port returns "" per the URL spec (standard ports are omitted). The empty string is falsy, so VITE_HMR_CLIENT_PORT becomes undefined. The sandbox's vite.config.js:28 then defaults to clientPort: 3000, causing the HMR client to connect on port 3000 -- which won't work in production.

Suggested change
VITE_HMR_CLIENT_PORT: url.port ? `${url.port}` : undefined
VITE_HMR_CLIENT_PORT: url.port || (url.protocol === 'https:' ? '443' : '80')

"assets": {
"binding": "Assets",
"not_found_handling": "none",
"run_worker_first": ["/*", "!/assets]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Malformed JSONC: missing closing " and ]. Users who copy-paste this will get a parse error.

Suggested change
"run_worker_first": ["/*", "!/assets]
"run_worker_first": ["/*", "!/assets"]

@ask-bonk
Copy link
Copy Markdown
Contributor

ask-bonk bot commented Mar 24, 2026

All review comments have been posted successfully. Here's a summary of what I reviewed and posted on PR #434:

Main review comment covering all findings, plus 3 inline comments on specific lines:

  1. Bug: Startup failure creates broken state (src/worker.js:33) - exposePort() is called before the server is verified running. If startup fails, the port stays exposed with no server, and subsequent requests skip the startup block. Suggested moving exposePort() after waitForPort().

  2. Bug: HMR broken in production (src/worker.js:41) - url.port returns "" on standard ports (443/80), causing VITE_HMR_CLIENT_PORT to be undefined and defaulting to 3000. Suggested fallback: url.port || (url.protocol === 'https:' ? '443' : '80').

  3. Bug: Malformed JSONC in README (README.md:58) - Missing closing quote and bracket in the troubleshooting example. Provided a suggestion fix.

  4. Minor: Stray value.js in the example root directory that isn't referenced anywhere.

  5. Minor: Missing trailing newline in sandbox-app/index.html.

I deliberately did not re-raise the biome.json formatter concern from earlier reviews since that change is no longer part of this PR. I also confirmed the Docker version (0.7.20) matches the current SDK version, and the static sandbox ID ('vite-sandbox') follows the pattern used by most other examples (minimal, desktop, alpine, time-machine, etc.).

github run

ghostwriternr
ghostwriternr previously approved these changes Mar 26, 2026
@ghostwriternr ghostwriternr disabled auto-merge March 26, 2026 14:25
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 26, 2026

🐳 Docker Images Published

Variant Image
Default cloudflare/sandbox:0.0.0-pr-434-0365672
Python cloudflare/sandbox:0.0.0-pr-434-0365672-python
OpenCode cloudflare/sandbox:0.0.0-pr-434-0365672-opencode
Musl cloudflare/sandbox:0.0.0-pr-434-0365672-musl
Desktop cloudflare/sandbox:0.0.0-pr-434-0365672-desktop

Usage:

FROM cloudflare/sandbox:0.0.0-pr-434-0365672

Version: 0.0.0-pr-434-0365672


📦 Standalone Binary

For arbitrary Dockerfiles:

COPY --from=cloudflare/sandbox:0.0.0-pr-434-0365672 /container-server/sandbox /sandbox
ENTRYPOINT ["/sandbox"]

Download via GitHub CLI:

gh run download 23601722748 -n sandbox-binary

Extract from Docker:

docker run --rm cloudflare/sandbox:0.0.0-pr-434-0365672 cat /container-server/sandbox > sandbox && chmod +x sandbox

url.port returns empty string for standard ports (443/80), causing
the sandbox HMR client to fall back to the wrong port. Resolve the
port explicitly so WebSocket connections work in production.
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 new potential issues.

View 11 additional findings in Devin Review.

Open in Devin Review

import react from '@vitejs/plugin-react';
import { defineConfig } from 'vite';

function incrementCounter() {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Function named incrementCounter actually decrements the counter

In sandbox-app/vite.config.js:5, the function is named incrementCounter but its body decrements: count-- at line 14. The README confirms the intended behavior is decrement ("The counter in the iframed sandbox will decrement once per second"), so the function name is wrong. This violates the repository's naming clarity principle and will confuse anyone reading the example code.

Suggested change
function incrementCounter() {
function decrementCounter() {
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +1 to +48
import { getSandbox, proxyToSandbox } from '@cloudflare/sandbox';

export { Sandbox } from '@cloudflare/sandbox';

const VITE_PORT = 5173;
const VITE_BASE = '/_/';

export default {
async fetch(request, env) {
const proxiedResponse = await proxyToSandbox(request, env);
if (proxiedResponse) {
return proxiedResponse;
}

const url = new URL(request.url);

if (url.pathname === '/api/sandbox') {
return handleAPISandboxRoute(url, env);
}

return new Response('Not Found', { status: 404 });
}
};

async function handleAPISandboxRoute(url, env) {
const sandbox = getSandbox(env.Sandbox, 'vite-sandbox');

let port = await sandbox
.getExposedPorts(url.host)
.then((ports) => ports.find((p) => p.port === VITE_PORT));

if (!port) {
port = await sandbox.exposePort(VITE_PORT, { hostname: url.host });

const process = await sandbox.startProcess('npm run dev', {
processId: 'vite-dev-server',
cwd: '/app',
env: {
VITE_BASE: VITE_BASE,
VITE_PORT: `${VITE_PORT}`,
VITE_HMR_CLIENT_PORT:
url.port || (url.protocol === 'https:' ? '443' : '80')
}
});
await process.waitForPort(VITE_PORT);
}

return Response.json({ url: `${port.url.replace(/\/$/, '')}${VITE_BASE}` });
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Example uses JavaScript instead of TypeScript, violating CONTRIBUTING.md rule

CONTRIBUTING.md states under Code Style: "Use TypeScript for all code." Every other example in the repository (examples/alpine, examples/minimal, examples/desktop, examples/openai-agents, etc.) uses TypeScript (.ts/.tsx), but the new vite-sandbox example uses JavaScript (.js/.jsx) for all its source files: src/worker.js, src/App.jsx, src/Counter.jsx, src/main.jsx, and src/value.js. This is inconsistent with the repository's explicit coding standard and established conventions.

Prompt for agents
Rename all JavaScript files in examples/vite-sandbox/src/ from .js/.jsx to .ts/.tsx to match the TypeScript convention used by every other example in the repository. Specifically:
- examples/vite-sandbox/src/worker.js → worker.ts
- examples/vite-sandbox/src/App.jsx → App.tsx
- examples/vite-sandbox/src/Counter.jsx → Counter.tsx
- examples/vite-sandbox/src/main.jsx → main.tsx
- examples/vite-sandbox/src/value.js → value.ts
Add appropriate type annotations (e.g., typing the env parameter in the worker fetch handler, adding React component return types). Update the HTML file at examples/vite-sandbox/index.html to reference src/main.tsx instead of src/main.jsx. Add a tsconfig.json to the example. Also update the package.json to include typescript as a devDependency.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@ghostwriternr ghostwriternr merged commit b5df781 into main Mar 26, 2026
34 of 39 checks passed
@ghostwriternr ghostwriternr deleted the example-vite-devserver branch March 26, 2026 15:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants