fix(instrumentation): defer ssrLoadModule to post-middleware hook#289
Conversation
Vite 7's SSRCompatModuleRunner requires the SSR environment's transport channel to be initialized before ssrLoadModule() can be called. During configureServer(), the channel is not yet ready, causing a TypeError when instrumentation.ts exists. Move the runInstrumentation() call from the configureServer body into the returned post-middleware function, where environments are fully initialized. Fixes cloudflare#167
There was a problem hiding this comment.
Pull request overview
This PR fixes a Vite 7 dev-server startup crash when a project contains instrumentation.ts by deferring the ssrLoadModule()-based instrumentation load until Vite’s SSR transport channel is initialized.
Changes:
- Move
runInstrumentation()invocation out of theconfigureServerbody and into the returned post-middleware hook. - Add regression tests ensuring
runInstrumentation()doesn’t throw on Vite SSR transport/channel errors and that it invokesregister()when exported.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
packages/vinext/src/index.ts |
Defers instrumentation module loading to Vite’s post-configureServer hook to avoid SSR transport initialization timing issues. |
tests/features.test.ts |
Adds regression coverage for runInstrumentation error handling and register() invocation. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Remove unreachable .catch() on runInstrumentation() since internal try/catch already handles errors and the function never rejects - Mock console.error in transport error test to suppress noisy output and assert the expected error message - Rename misleading test to accurately describe what it verifies
|
/bigbonk review this please |
commit: |
There was a problem hiding this comment.
Review
The core fix is correct — moving runInstrumentation() into the returned post-middleware function so that ssrLoadModule() is called after Vite's SSR transport channel is initialized. Good diagnosis of the root cause.
A few issues worth addressing:
|
Review posted successfully. Here's a summary of the feedback: The core fix is correct — moving Three comments posted:
|
southpolesteve
left a comment
There was a problem hiding this comment.
Review: fix(instrumentation): defer ssrLoadModule to post-middleware hook
The core fix is correct and well-motivated. Moving runInstrumentation() from the configureServer body into the returned post-middleware function is the right approach for the Vite 7 SSR transport timing issue. The author has addressed all of bigbonk's feedback (re-added .catch(), removed the duplicate test, tightened the assertion).
Correctness
The fix is correct. In Vite's createServer(), the lifecycle is:
configureServerhooks are called (collecting post-hooks)- Internal middlewares are installed (SSR transport channel initialized here)
- Post-hooks are called synchronously via
postHooks.forEach(fn => fn && fn()) listen()is called later
The original code called ssrLoadModule() during step 1, before the SSR transport channel existed. The fix moves it to step 3, where the channel is ready. This directly addresses the TypeError: Cannot read properties of undefined (reading 'outsideEmitter') crash from issue #167.
Next.js parity
Next.js docs state that register() is called "once when a new Next.js server instance is initiated" and "must complete before the server is ready to handle requests." The current implementation fires runInstrumentation() without await, so it's a fire-and-forget async call. This means there's a theoretical race where early requests could arrive before register() completes.
However, this is a pre-existing issue, not introduced by this PR. The old code had the same fire-and-forget pattern (just in a different location where it crashed). And in practice, the post-hook runs during createServer() before listen() is called, so register() will almost certainly complete before any request arrives. This is acceptable for a dev server.
Non-blocking note (pre-existing, out of scope): For strict Next.js parity, runInstrumentation should ideally be awaited to guarantee register() completes before the first request. But the post-hook function is synchronous (Vite doesn't await it), so this would require a different approach (e.g., a middleware that blocks until instrumentation is ready). Not worth blocking this PR over. Could be filed as a separate enhancement.
Security
No security concerns. Deferring instrumentation loading by a few milliseconds during dev server startup doesn't create an exploitable window. Instrumentation is for observability (Sentry, OpenTelemetry), not for security enforcement. Even if a request arrived before register() completed, the only consequence would be that one request wouldn't be traced.
Dev/prod parity
No parity issue. I checked all four server files:
server/prod-server.ts: No instrumentation loading at all (production server doesn't usessrLoadModule)cloudflare/index.ts: No instrumentation handlingserver/dev-server.ts: Only importsreportRequestError(the consumer side), notrunInstrumentationserver/app-dev-server.ts: Same, only importsreportRequestError
The runInstrumentation call only exists in index.ts (the Vite plugin's configureServer hook), which is dev-only. The production path doesn't have this issue because it doesn't use Vite's SSR module runner.
Pre-existing note (out of scope): prod-server.ts and the Cloudflare worker entry don't call runInstrumentation at all, meaning instrumentation.ts is completely ignored in production. This is a separate gap worth tracking but not introduced by this PR.
Test quality
The new test is good: it verifies that runInstrumentation handles the specific outsideEmitter TypeError gracefully (catches it, logs it, doesn't crash). The assertion is tight (exact error message match) and console.error is properly spied/restored.
As bigbonk correctly noted (and the author acknowledged), a unit test with mocks can't verify the actual timing behavior (that ssrLoadModule works in the post-hook but not in configureServer). That would require an integration test against a real Vite dev server. The existing test coverage in instrumentation.test.ts already covers the core runInstrumentation behavior thoroughly (register called, onRequestError stored, missing exports handled, ssrLoadModule failures caught).
Edge cases
- No instrumentation file: Handled by the
if (instrumentationPath)guard. No change in behavior. - instrumentation.ts throws: Handled by
runInstrumentation's internal try/catch plus the outer.catch()safety net. Logs error, continues. - Slow instrumentation: Same fire-and-forget behavior as before. Early requests might not have instrumentation active. Acceptable for dev.
Approving. Clean fix for a real crash, good diagnosis, feedback addressed. The pre-existing gaps (fire-and-forget timing, no production instrumentation support) should be tracked separately.
Summary
vite devcrashes immediately when a project hasinstrumentation.tsrunInstrumentation()call from theconfigureServerbody into the returned post-middleware function, where Vite's SSR environment transport channel is fully initializedregister()invocationRoot Cause
Vite 7's
SSRCompatModuleRunnerrequires the SSR environment's transport channel beforessrLoadModule()can be called. DuringconfigureServer(), the channel isn't ready yet →TypeError: Cannot read properties of undefined (reading 'outsideEmitter').Changes
packages/vinext/src/index.tsrunInstrumentationfrom configureServer body into returned post-middleware functiontests/features.test.tsTest plan
pnpm test— all 2204 tests pass (2 pre-existing failures in static-export/ecosystem unrelated to this change)pnpm run typecheck— no new type errorspnpm run test:e2e— no regressionsinstrumentation.ts, runvite dev, confirm no crash/bonk