Skip to content

[vite-plugin] Dispose remote proxy sessions when the server closes - #15269

Open
official-burak wants to merge 3 commits into
cloudflare:mainfrom
official-burak:fix/dispose-remote-proxy-sessions
Open

[vite-plugin] Dispose remote proxy sessions when the server closes#15269
official-burak wants to merge 3 commits into
cloudflare:mainfrom
official-burak:fix/dispose-remote-proxy-sessions

Conversation

@official-burak

@official-burak official-burak commented Aug 19, 2026

Copy link
Copy Markdown

Fixes #15173.

With remoteBindings enabled, vite build writes every file correctly and then hangs forever. Each remote proxy session runs a listening server, which is a referenced libuv handle, so the event loop cannot drain while one stays open.

Sessions are cached in a module-level map in miniflare-options.ts, keyed by config path. That map was only ever read and written, never cleared, and the one existing session.dispose() call fires solely to replace a session whose auth has changed. Nothing tore sessions down when the prerender pass finished and the preview server closed, so the build could not exit. This makes remote bindings unusable in any CI build.

This adds disposeRemoteProxySessions() and calls it when the dev and preview servers close, alongside the existing ctx.disposeMiniflare(). The preview path is the one that fixes the reported hang, since during vite build the preview server exists only to serve the prerender pass. The dev path leaks the same handle when the server is closed programmatically, so it is covered too; that call sits inside the existing !ctx.isRestartingDevServer guard, so sessions are still reused across dev server restarts.

Disposal failures are swallowed through debuglog, matching how the neighbouring Miniflare disposal already behaves, so a failing teardown cannot stop the server from closing.


  • Tests
    • Tests included/updated
    • Automated tests not possible - manual testing has been completed as follows:
    • Additional testing not necessary because:
  • Public documentation
    • Cloudflare docs PR(s):
    • Documentation not necessary because: this fixes a hang in existing behaviour and adds no user-facing API or configuration.

Test evidence

Added disposes the remote proxy session when preview server is closed to preview-server.spec.ts, next to the existing Miniflare disposal test. It mocks maybeStartOrUpdateRemoteProxySession so no account or network access is needed, builds the fixture worker, starts a preview server with remoteBindings: true, and asserts the session is disposed on close.

Before the change:

 ❯ src/__tests__/preview-server.spec.ts (2 tests | 1 failed)
     ✓ disposes Miniflare when preview server is closed 100ms
     × disposes the remote proxy session when preview server is closed 65ms

AssertionError: expected "vi.fn()" to be called at least once
 ❯ src/__tests__/preview-server.spec.ts:100:19

After the change, the full package suite passes:

 Test Files  21 passed (21)
      Tests  199 passed (199)

oxlint --deny-warnings --type-aware reports 0 warnings and 0 errors, oxfmt --check passes on the changed files, and tsc --build is clean.

Note

This is a contribution from an AI agent: Claude Opus 5. The investigation, fix and tests were produced with AI assistance, and reviewed and owned by me, Burak Keskin. I will be responding on this PR.


Open in Devin Review

With `remoteBindings` enabled, `vite build` wrote every file correctly and then
hung forever. Each remote proxy session runs a listening server, which is a
referenced libuv handle, so the event loop cannot drain while one stays open.

Sessions are cached in a module-level map keyed by config path. That map was
only ever read and written, never cleared, and the single existing
`session.dispose()` call fires solely to replace a session whose auth changed.
Nothing tore sessions down when the prerender pass finished and the preview
server closed, so the build could not exit. This made remote bindings unusable
in CI builds.

Dispose the sessions and clear the map when the dev or preview server closes.
The dev path is guarded by the existing restart check, so sessions are still
reused across dev server restarts.
@changeset-bot

changeset-bot Bot commented Aug 19, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: a531542

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@cloudflare/vite-plugin Patch

Not sure what this means? Click here to learn what changesets are.

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

@github-project-automation github-project-automation Bot moved this to Untriaged in workers-sdk Aug 19, 2026
@workers-devprod
workers-devprod requested review from a team and NuroDev and removed request for a team August 19, 2026 13:54
@workers-devprod

Copy link
Copy Markdown
Contributor

Codeowners approval required for this PR:

  • @cloudflare/wrangler
Show detailed file reviewers
  • .changeset/spicy-donkeys-listen.md: [@cloudflare/wrangler]
  • packages/vite-plugin-cloudflare/src/tests/preview-server.spec.ts: [@cloudflare/wrangler]
  • packages/vite-plugin-cloudflare/src/miniflare-options.ts: [@cloudflare/wrangler]
  • packages/vite-plugin-cloudflare/src/plugins/dev.ts: [@cloudflare/wrangler]
  • packages/vite-plugin-cloudflare/src/plugins/preview.ts: [@cloudflare/wrangler]

devin-ai-integration[bot]

This comment was marked as resolved.

@pkg-pr-new

pkg-pr-new Bot commented Aug 19, 2026

Copy link
Copy Markdown
@cloudflare/autoconfig

npm i https://pkg.pr.new/@cloudflare/autoconfig@15269

@cloudflare/build-output-utils

npm i https://pkg.pr.new/@cloudflare/build-output-utils@15269

@cloudflare/codemods

npm i https://pkg.pr.new/@cloudflare/codemods@15269

@cloudflare/config

npm i https://pkg.pr.new/@cloudflare/config@15269

create-cloudflare

npm i https://pkg.pr.new/create-cloudflare@15269

@cloudflare/deploy-helpers

npm i https://pkg.pr.new/@cloudflare/deploy-helpers@15269

@cloudflare/kv-asset-handler

npm i https://pkg.pr.new/@cloudflare/kv-asset-handler@15269

miniflare

npm i https://pkg.pr.new/miniflare@15269

@cloudflare/pages-functions

npm i https://pkg.pr.new/@cloudflare/pages-functions@15269

@cloudflare/pages-shared

npm i https://pkg.pr.new/@cloudflare/pages-shared@15269

@cloudflare/unenv-preset

npm i https://pkg.pr.new/@cloudflare/unenv-preset@15269

@cloudflare/vite-plugin

npm i https://pkg.pr.new/@cloudflare/vite-plugin@15269

@cloudflare/vitest-plugin

npm i https://pkg.pr.new/@cloudflare/vitest-plugin@15269

@cloudflare/workers-auth

npm i https://pkg.pr.new/@cloudflare/workers-auth@15269

@cloudflare/workers-editor-shared

npm i https://pkg.pr.new/@cloudflare/workers-editor-shared@15269

@cloudflare/workers-utils

npm i https://pkg.pr.new/@cloudflare/workers-utils@15269

wrangler

npm i https://pkg.pr.new/wrangler@15269

commit: a531542

@official-burak

Copy link
Copy Markdown
Author

A note on the red checks, in case it saves triage time. As far as I can tell none of them relate to this change, and no test actually fails.

Vite Plugin E2E (Linux / macOS / Windows) — 44 tests passed, 0 failed. The two failed suites are the yarn package-manager variants, which fail while installing the fixture:

Error: Command failed: yarn install
error Couldn't find any versions for "@rolldown/binding-darwin-x64" that matches "1.2.5"
Error: Couldn't find any versions for "@rolldown/binding-linux-arm64-gnu" that matches "1.2.5"

rolldown@1.2.5 is published and resolves as latest, but its platform binding packages are not: @rolldown/binding-darwin-x64 and @rolldown/binding-linux-arm64-gnu are both still at 1.2.4 on npm. npm and pnpm skip optional platform packages that do not match the current platform, while yarn classic resolves all of them and fails hard, which is why only the yarn variants break. This should clear once those bindings are published.

Tests (Windows, packages-and-tools) — 1063 miniflare tests passed; the single failed suite is test/plugins/browser/index.spec.ts, where Chrome did not come up on the runner: Chrome readiness probe at http://127.0.0.1:50975/json/version timed out after 5000ms.

Vite Plugin Playground (macOS, vite-8) — 287 tests passed; one beforeAll hook timed out after 50s bringing up the react-spa server.

Tests (Linux, packages-and-tools) is the job that covers this package and it passes, including the added test:

@cloudflare/vite-plugin:test:ci:  ✓ src/__tests__/preview-server.spec.ts (2 tests) 620ms
@cloudflare/vite-plugin:test:ci:  Test Files  21 passed (21)
@cloudflare/vite-plugin:test:ci:       Tests  199 passed (199)

Happy to rebase or re-run whenever that is useful.

Per REVIEW.md, changesets should target users rather than maintainers.
Drop the event-loop and session-disposal internals and state the observable
behaviour instead.

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

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.

Devin Review found 1 new potential issue.

View 2 additional findings in Devin Review.

Open in Devin Review

Comment thread packages/vite-plugin-cloudflare/src/miniflare-options.ts
@official-burak

Copy link
Copy Markdown
Author

Follow-up on the latest run after the changeset rewrite: the only remaining red check is Vite Plugin E2E (Windows).

Linux and macOS both completed with 9/9 test files passing. On Windows the suite was still progressing (completed files were passing; yarn installs now succeed) when the job hit the 20-minute step timeout:

##[error]The action 'Run Vite plugin E2E tests' has timed out after 20 minutes.

That timeout is showing up on other unrelated PRs today as well (for example the hyperdrive PlanetScale PR, where Windows also fails while Linux/macOS pass). Nothing in this change looks involved: teardown in the logs completes in ~100ms, and Tests (Windows, packages-and-tools) including the new preview-server.spec.ts still passes.

Happy to rebase or wait on a re-run.

} catch (error) {
debuglog("Failed to dispose Miniflare instance:", error);
}
await disposeRemoteProxySessions();

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.

What happens if this throws?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

It would have failed viteDevServer.close(). Unlike disposeMiniflare() just above it, this call was not wrapped, so a throw from disposeRemoteProxySessions() skipped the rest of teardown.

session.dispose() is still caught per session. The call site now matches the Miniflare try/catch, so a teardown error is logged and cannot fail the close.

*/
export async function disposeRemoteProxySessions(): Promise<void> {
const remoteProxySessionsData = [...remoteProxySessionsDataMap.values()];
remoteProxySessionsDataMap.clear();

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.

Wouldn't it be better to only clear this map after we're certain that the session disposition call passed for all sessions?
That way we could even add retries if it does fail for whatever reason.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Yes. The previous version copied the values and cleared the map first, so a failed dispose() left a listening handle with nothing to retry against.

Pushed a531542: the map entry is deleted only after session.dispose() resolves. A later close, or a subsequent start that still reads the map, can retry. Added a test that rejects the first dispose and checks the session is still passed through as pre-existing.

Comment thread packages/vite-plugin-cloudflare/src/miniflare-options.ts
Clearing the map before dispose ran meant a failed session.dispose()
dropped the only handle we had, so a later close could not retry and
the listening server could keep the event loop alive. Swallow dispose
errors on the close path the same way Miniflare already does.
@workers-devprod

Copy link
Copy Markdown
Contributor

Codeowners approval required for this PR:

  • @cloudflare/wrangler
Show detailed file reviewers
  • .changeset/spicy-donkeys-listen.md: [@cloudflare/wrangler]
  • packages/vite-plugin-cloudflare/src/tests/preview-server.spec.ts: [@cloudflare/wrangler]
  • packages/vite-plugin-cloudflare/src/miniflare-options.ts: [@cloudflare/wrangler]
  • packages/vite-plugin-cloudflare/src/plugins/dev.ts: [@cloudflare/wrangler]
  • packages/vite-plugin-cloudflare/src/plugins/preview.ts: [@cloudflare/wrangler]

@official-burak

Copy link
Copy Markdown
Author

CI update: the Windows Vite Plugin E2E timeout from the earlier run is gone. Latest checks on this PR are green (59/59 on the previous SHA). Pushed a531542 for the dispose-map review comments; that SHA is still running.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Untriaged

Development

Successfully merging this pull request may close these issues.

[vite-plugin] vite build never exits when remoteBindings is enabled — remote proxy session is never disposed

3 participants