cloudflare / cloudflare/workers-sdk
[vite-plugin] vite build never exits when remoteBindings is enabled — remote proxy session is never disposed
- Dominant language
- TypeScript
- Stars
- 4.5k
- Forks
- 1.5k
- Avg merge
- 3d 8h
- Merged PRs (30d)
- 186
Description
With `remoteBindings` enabled, `vite build` produces all output correctly and then hangs forever. The process never exits.
### Environment
- `@cloudflare/vite-plugin@1.51.1`
- `wrangler@4.120.0`
- `vite@8.2.1`
- TanStack Start, prerendering enabled
- Worker has 7 Durable Object bindings and D1/KV/VPC bindings with `"remote": true`
### Reproduction
Run a build with `remoteBindings: true` in the plugin options and prerendering enabled.
The remote connection is established fine (no error 10375, so #13956 is genuinely fixed — thank you). The prerender pass completes:
```
⎔ Establishing remote connection...
[prerender] Prerendering pages...
[prerender] Crawling: /
GET / 200 OK (89ms)
...
[prerender] Prerendered 117 pages:
```
Every file is written to `dist/`, the output is correct, and then the process just sits there. It has to be killed manually.
### Cause
`startRemoteProxySession` in wrangler creates a full `DevEnv` with a listening server:
```js
const workerConfig = {
name: options.workerName ?? crypto2.randomUUID(),
entrypointSource: ProxyServerWorker_default,
...
server: { port: 0, secure: false }
};
let devEnv;
try {
devEnv = new DevEnv(workerConfig);
devEnv.start();
}
```
That listening server is a referenced libuv handle, so Node's event loop cannot drain while it stays open.
On the plugin side, sessions are kept in a module-level map:
```js
/** Map that maps worker configPaths to their existing remote proxy session data (if any) */
const remoteProxySessionsDataMap = new Map();
```
Grepping the built plugin (`dist/index.mjs` of 1.51.1), that map has **2 `get`s and 2 `set`s** (one pair on the dev server path, one on the preview server path) and **zero `delete`s**. The only `session.dispose()` call in the whole bundle is:
```js
if (!authSameAsBefore) {
if (preExistingRemoteProxySessionData?.session)
await preExistingRemoteProxySessionData.session.dispose();
remoteProxySession = await startSession(remoteBindings, { ... });
}
```
It only fires to *replace* a session when auth changes. There is no teardown when the preview server closes or when the build ends.
For `vite dev` this is invisible, since the server runs until you interrupt it. For `vite build`, the preview server exists only to serve the prerender pass, so once the pass finishes nothing disposes the session and the build hangs.
### Expected
The preview server's remote proxy session should be disposed when the build completes, so `vite build` exits normally.
### Impact
Remote bindings cannot be used in any CI build. That rules out prerendering pages against real data, which is otherwise exactly what remote bindings make possible. In our case the prerendered output was perfect (real values from D1 dehydrated into the static HTML) and the only thing standing in the way is the process not exiting.
I could not find an existing issue for this, and the `1.52.0` changelog does not appear to touch session disposal or build exit.
Contributor guide
Research direction
Start by tracing startRemoteProxySession and the remoteProxySessionsDataMap usage described in dist/index.mjs, then inspect the preview-server and build teardown paths. Done means the preview session is disposed after prerendering and vite build exits normally with remoteBindings enabled.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript, vite
- Domain
- build-system, tooling
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 68/100