Skip to content

fix(eval): vite-node-specific defense against the server-close race that throws ERR_CLOSED_SERVER#203

Open
Hiroki Osame (privatenumber) wants to merge 6 commits into
braintrustdata:mainfrom
privatenumber:fix-eval-vite-node
Open

fix(eval): vite-node-specific defense against the server-close race that throws ERR_CLOSED_SERVER#203
Hiroki Osame (privatenumber) wants to merge 6 commits into
braintrustdata:mainfrom
privatenumber:fix-eval-vite-node

Conversation

@privatenumber
Copy link
Copy Markdown

@privatenumber Hiroki Osame (privatenumber) commented May 22, 2026

Problem

bt eval --runner vite-node <file>.ts fails with error: eval runner exited with status exit status: 1 for projects whose Vite config enables dev.recoverable for SSR (Vite wrappers like vite-plus do by default). Invoking the cached eval-runner directly surfaces the real error: ERR_CLOSED_SERVER from vite's transformRequest.

vite-node's CLI does await runner.executeFile(file); await server.close(); (cli.ts:146-152). executeFile resolves the moment vite-node's async wrapper around the entry returns, which is immediately if the entry uses fire-and-forget. The eval-runner used main().catch(...) at the top level, so vite-node tore down the dev server while main() was still resolving await import() calls.

Same bug class as vitejs/vite#13786. The pattern has been latent since vite-node support landed; it surfaces now because configs that re-enable dev.recoverable reopen the closed-server guard that #13787 silenced for SSR by default.

tests/evals/js/eval-vite-node-server-close-race/ reproduces it deterministically.

Changes

Only vite-node's CLI needs top-level await; every other runner (tsx, bun, deno, ts-node, plain Node) has event-loop semantics where fire-and-forget is the natural pattern. The runner is split to reflect that:

  • scripts/eval-runner-impl.ts — shared impl. Exports main().
  • scripts/eval-runner.mts — vite-node entry: imports main and awaits it. .mts forces ESM at the loader for tsx/Bun/Deno; vite-node is format-agnostic.
  • Default .ts entry generated by js_runner_default_source(): impl body with main().catch(...) appended. Standalone (no import) to avoid a Deno-vs-ts-node .ts-extension conflict.
  • src/eval.rsjs_runner_path_for_kind swaps the runner script to .mts only for RunnerKind::ViteNode.
  • Unit tests pin the entry shapes so the fire-and-forget vs TLA asymmetry can't silently drift.

@privatenumber Hiroki Osame (privatenumber) marked this pull request as ready for review May 22, 2026 07:48
@privatenumber Hiroki Osame (privatenumber) changed the title fix(eval): bt eval --runner vite-node fails with ERR_CLOSED_SERVER fix(eval): vite-node-specific defense against the server-close race that throws ERR_CLOSED_SERVER May 22, 2026
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.

1 participant