Skip to content

[plugin-rsc] Root identity probe triggers mid-request dependency optimization #1473

Open

Description

@james-elicx

Bug

The first cold request to a next-intl app under @cloudflare/vite-plugin returns 500 with Invalid hook call / Cannot read properties of null (reading 'use'). The next request succeeds.

The trigger is the second resolution in rsc:virtual-client-package. The actual import resolves from inside node_modules; the identity check resolves again from <root>/index.html. For root-resolvable transitive dependencies, that second lookup calls Vite's registerMissingImport, causing optimization and a reload during rendering. Here it discovers @formatjs/intl-localematcher, negotiator, @formatjs/fast-memoize, and intl-messageformat.

Reproduction

Application files and configuration are in the downstream report. Use its npm installation, next-intl navigation Link, client translation component, and locale middleware/proxy.

rm -rf node_modules/.vite .vinext
npm run dev  # port 3101

From another terminal, without opening the app first:

curl -s -o /dev/null -w '%{http_code}\n' http://localhost:3101/en # 500
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:3101/en # 200

Expected: the first request succeeds; a package-identity check does not schedule dependency optimization.

Tested package patch

This is the exact patch tested against published plugin-rsc 0.5.35, with published vinext unchanged. Save as patches/@vitejs+plugin-rsc+0.5.35.patch for patch-package, or apply with git apply from the app root.

diff --git a/node_modules/@vitejs/plugin-rsc/dist/plugin-CLvIS10V.js b/node_modules/@vitejs/plugin-rsc/dist/plugin-CLvIS10V.js
--- a/node_modules/@vitejs/plugin-rsc/dist/plugin-CLvIS10V.js
+++ b/node_modules/@vitejs/plugin-rsc/dist/plugin-CLvIS10V.js
@@ -1511,17 +1511,26 @@
 			resolveId: {
 				order: "pre",
 				async handler(source, importer, options) {
+                    if (options.custom?.["rsc:package-identity-probe"]) return;
 					if (this.environment.name === serverEnvironmentName && bareImportRE.test(source) && !(source === "client-only" || source === "server-only")) {
 						const resolved = await this.resolve(source, importer, options);
 						if (resolved && resolved.id.includes("/node_modules/")) {
 							const rootImporter = path.join(this.environment.config.root, "index.html");
-							let resolvedAtRoot;
+							const scanRoot = this.environment.mode === "dev" && !this.environment.config.optimizeDeps.noDiscovery;
+                            let resolvedAtRoot;
 							try {
-								resolvedAtRoot = await this.resolve(source, rootImporter, options);
+								resolvedAtRoot = scanRoot
+                                    ? await this.environment.pluginContainer.resolveId(source, rootImporter, {
+                                        ...options,
+                                        scan: true,
+                                        custom: { ...options.custom, "rsc:package-identity-probe": true },
+                                      })
+                                    : await this.resolve(source, rootImporter, options);
 							} catch {
 								return;
 							}
-							if (!resolvedAtRoot || resolvedAtRoot.id !== resolved.id) return;
+							if (!resolvedAtRoot || (resolvedAtRoot.id !== resolved.id &&
+                                (!scanRoot || resolvedAtRoot.id !== resolved.id.replace(/\?v=[a-f\d]+$/, "")))) return;
 							packageSources.set(resolved.id, source);
 							return resolved;
 						}

The patch keeps normal import resolution and the root-identity guard, bypasses recursive probes, and tolerates only Vite's terminal version query. Build mode and explicit noDiscovery: true retain the existing path.

Proposed upstream fix: give this diagnostic lookup a supported way to resolve without discovering optimized dependencies. The prototype uses the internal container scan option; passing it to this.resolve() alone does not work because Vite inherits the caller's scan state.

Validation

Four clean starts failed before the patch; all four succeeded after changing plugin-rsc alone. Restoring the package restored the failure. Tested Cloudflare compatibility dates: 2026-09-27 and 2026-02-12. Eight focused resolver checks and a production build also passed; this does not establish browser-hydration or full upstream-suite compatibility.

Versions: macOS arm64, Node 24.21.0, npm, Vite 8.3.1, plugin-rsc 0.5.35, Cloudflare Vite plugin 1.61.0, next-intl 4.14.7, React 19.3.0, vinext 1.0.0-beta.13.

Related: #1273. This identifies a specific trigger for mid-request optimization.

Activity

  1. james-elicx commented on Sep 27, 2026

    @james-elicx
    ContributorAuthor

    I think it would be nice for the probe to work without triggering re-optimisation. Is that something you're open to exploring @hi-ogawa ?

  2. hi-ogawa commented on Sep 28, 2026

    @hi-ogawa
    Contributor

    I think it would be nice for the probe to work without triggering re-optimisation. Is that something you're open to exploring @hi-ogawa ?

    I have a same sentiment that this.resolve can be invasive for server deps optimization. We added 2nd root this.resolve there recently for some edge case and wondering if that's what triggers error, or was the 1st one already the culprit? Either way, I'm fine with adjusting some condition around there specifically for noDiscovery = false case. I cannot tell yet if the above patch is reasonable a fix for that.

  3. added theissue type on Sep 28, 2026
  4. james-elicx commented on Sep 30, 2026

    @james-elicx
    ContributorAuthor

    I think it would be nice for the probe to work without triggering re-optimisation. Is that something you're open to exploring @hi-ogawa ?

    I have a same sentiment that this.resolve can be invasive for server deps optimization. We added 2nd root this.resolve there recently for some edge case and wondering if that's what triggers error, or was the 1st one already the culprit? Either way, I'm fine with adjusting some condition around there specifically for noDiscovery = false case. I cannot tell yet if the above patch is reasonable a fix for that.

    I think it is the second one that triggers it. cloudflare/vinext#3510 was what led me to making this issue.

    When my agent was debugging, it found the following:

    RESOLVE_IMPORTER @formatjs/fast-memoize /private/var/folders/rs/3g0tqsyd4379793y7wlcghth0000gn/T/vinext-3510-exact-03codgn8/node_modules/use-intl/dist/esm/development/formatters-r4aAmsMP.js?v=c2bf7685
    RESOLVE_IMPORTER @formatjs/fast-memoize /private/var/folders/rs/3g0tqsyd4379793y7wlcghth0000gn/T/vinext-3510-exact-03codgn8/index.html
    REGISTER [
      '@formatjs/fast-memoize',
      '/private/var/folders/rs/3g0tqsyd4379793y7wlcghth0000gn/T/vinext-3510-exact-03codgn8/node_modules/@formatjs/fast-memoize/index.js'
    ] Error
        at o.registerMissingImport (file:///private/var/folders/rs/3g0tqsyd4379793y7wlcghth0000gn/T/vinext-3510-exact-03codgn8/node_modules/.vite-temp/vite.config.ts.timestamp-1790539874960-e4c161b91d43b8.mjs:15:104)
        at viteResolvePlugin.finalizeBareSpecifier (file:///private/var/folders/rs/3g0tqsyd4379793y7wlcghth0000gn/T/vinext-3510-exact-03codgn8/node_modules/vite/dist/node/chunks/node.js:28963:42)
    9:11:16 PM [vite] (rsc) dependency optimized: @formatjs/fast-memoize
    9:11:16 PM [vite] (rsc) optimized dependencies changed. reloading
    

    So it looks like it resolves the actual module, then resolves again from index.html, registers it for optimisation, and reloads.

    So I think if the probe wasn't happening, this wouldn't be an issue?

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

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions