Skip to content

fix(void): restore Container Durable Object bindings in the dev server - #696

Merged
cpojer merged 1 commit into
voidzero-dev:mainfrom
morinokami:fix/dev-container-bindings
Nov 4, 2026
Merged

cpojer merged 1 commit into
voidzero-dev:mainfrom
morinokami:fix/dev-container-bindings

Conversation

@morinokami

Copy link
Copy Markdown
Contributor

Since 1.5.0, an app that uses void/sandbox can't reach its Sandbox in the dev server. getSandbox() throws:

env: Missing Cloudflare binding 'SANDBOX'.

Other Container classes lose their binding too. 1.4.0 works.

Since 8b26dba, the config for @cloudflare/vite-plugin 2.0 comes from migrateBuiltWranglerConfig(), which rewrites the binding of each Container class as unsafe:durable_object_namespace. That form is only for the Worker upload. Miniflare skips unsafe bindings that have no dev plugin, so the class and its Container still exist in dev, but env has no binding for them.

Fix

In serve, void:cloudflare-config turns these bindings back into regular durable-object bindings. Builds and deploys keep the unsafe form.

Only the generated binding of a Container class changes. An unsafe binding in the user's config has the same type, but it may point at another Worker, even at a class with the same name as a local Container. Turning it into a local binding would connect it to the local class instead, or stop workerd from starting if this Worker has no such class.

docs/guide/sandboxes.md already describes local development with Docker, so the docs do not change.

Not fixed here

These also regressed in 1.5.0:

  • vite preview with a Sandbox fails with the same error.
  • A Container with a Dockerfile image outside void/sandbox stops the dev server from starting.
  • A Container in an auxiliary Worker stops the dev server from starting.

Tests

packages/void/test/unit/plugin-cloudflare-v2.test.ts runs the real plugin and Miniflare with a Docker stub, so it needs no Docker. In the dev server, Container classes and a regular Durable Object are bound, and unsafe bindings from the user's config are not, including one that names another Worker's class with the same name as a local Container. In the build, Container classes keep the unsafe form.

The test fails without the fix, without the Container class check and without the generated-form check.

Verification

  • vp test run: 772 files pass, 1 skipped (15,810 tests pass, 29 skipped).
  • vp run check: 33/33 pass.
  • By hand under vp dev with Docker:
    • getSandbox('x') runs echo hi, and the local explorer lists SANDBOX.
    • The acceptance route of the kitchen-sink-sandbox fixture passes all 12 checks.
    • A renamed binding and class, a defineDurableObject class with a Container, and CLOUDFLARE_ENV pointing at an environment with only a name also work.
  • Not run: a real Cloudflare deploy. The change only runs in serve, and the test checks the binding forms in the build output.

Since 1.5.0, an app that uses `void/sandbox` can't reach its Sandbox in
the dev server. `getSandbox()` throws:

    env: Missing Cloudflare binding 'SANDBOX'.

Other Container classes lose their binding too. 1.4.0 works.

Since 8b26dba, the config for `@cloudflare/vite-plugin` 2.0 comes from
`migrateBuiltWranglerConfig()`, which rewrites the binding of each
Container class as `unsafe:durable_object_namespace`. That form is only
for the Worker upload. Miniflare skips unsafe bindings that have no dev
plugin, so the class and its Container still exist in dev, but `env` has
no binding for them.

## Fix

In serve, `void:cloudflare-config` turns these bindings back into regular
`durable-object` bindings. Builds and deploys keep the unsafe form.

Only the generated binding of a Container class changes. Unsafe bindings
in the user's config use the same type and may name another Worker, even
one with a class of the same name. A local binding would reach the wrong
class, or stop workerd from starting if this Worker doesn't define it.

## Not fixed here

These also regressed in 1.5.0:

- `vite preview` with a Sandbox fails with the same error.
- A Container with a Dockerfile `image` outside `void/sandbox` stops the
  dev server from starting.
- A Container in an auxiliary Worker stops the dev server from starting.

## Tests

`plugin-cloudflare-v2.test.ts` runs the real plugin and Miniflare with a
Docker stub, so it needs no Docker. In the dev server, Container classes
and a regular Durable Object are bound, and unsafe bindings from the
user's config are not, including one that names another Worker's class
with the same name as a local Container. In the build, Container classes
keep the unsafe form. The test fails without the fix and without either
check.

## Verification

- `vp test run`: 772 files pass, 1 skipped (15,810 tests pass, 29
  skipped).
- `vp run check`: 33/33 pass.
- By hand under `vp dev` with Docker: `getSandbox('x')` runs `echo hi`,
  the explorer lists `SANDBOX`, and the `kitchen-sink-sandbox` acceptance
  route passes all 12 checks. A renamed binding and class, a
  `defineDurableObject` class with a Container and `CLOUDFLARE_ENV` also
  work.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8F8mZpYEz7gFr91ZVxeVB
@cpojer
cpojer self-requested a review November 4, 2026 04:10

@cpojer cpojer left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Ahh, sorry for the breakage and thanks for the fix. We'll publish 1.6.0 with this later today (eta ~4 hours).

@cpojer
cpojer merged commit 65eb7f0 into voidzero-dev:main Nov 4, 2026
2 checks passed
Brooooooklyn added a commit that referenced this pull request Nov 4, 2026
…697)

Fixes a flaky unit run seen on #669: every test passed, but Vitest
failed the job on an uncaught `Error: write EPIPE` from `dockerBuild` in
`@cloudflare/vite-plugin` (test: "binds Container Durable Objects in the
dev server…", added in #696).

```
dockerBuild: spawn(docker build … -f -) → child.stdin.write(Dockerfile)

old stub   #!/bin/sh
           exit 0                    → on a busy runner the stub exits before the write
                                       → write EPIPE, no error listener → uncaught → job fails
new stub   if [ "$1" = build ]; then cat >/dev/null; fi
           exit 0                    → the stub waits until stdin closes, so the write always lands
```

Reproduced locally by blocking the parent between `spawn` and `write`
(same order as the CI trace):

| stub | result |
| --- | --- |
| old | `Error: write EPIPE` |
| new | ok |

Only `build` reads stdin, so other Docker calls cannot block on it.
Test-only change.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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.

2 participants