From ccf98a3d29cd0b0e2474eb982f6943c2ad407287 Mon Sep 17 00:00:00 2001 From: Nenad Bozidarevic Date: Wed, 21 Oct 2026 10:15:51 -0700 Subject: [PATCH] fix(rsc): skip unparseable request URLs in dev middleware The rsc:findSourceMapURL and vite-rsc-load-module-dev-proxy dev middlewares parse every request URL with new URL(). Requests like // or //[ are valid HTTP but do not parse, and the throw inside an async Connect middleware becomes an unhandled rejection that kills the whole dev server. Parse through a shared helper that returns undefined on failure; both middlewares pass the request down the chain instead. --- packages/plugin-rsc/src/plugin.ts | 7 ++++++- .../src/plugins/find-source-map-url.ts | 8 +++++-- .../plugin-rsc/src/plugins/vite-utils.test.ts | 21 +++++++++++++++++++ packages/plugin-rsc/src/plugins/vite-utils.ts | 12 +++++++++++ 4 files changed, 45 insertions(+), 3 deletions(-) create mode 100644 packages/plugin-rsc/src/plugins/vite-utils.test.ts diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts index 6cb24453e..db76ed28a 100644 --- a/packages/plugin-rsc/src/plugin.ts +++ b/packages/plugin-rsc/src/plugin.ts @@ -66,6 +66,7 @@ import { directRequestRE, evalValue, normalizeViteImportAnalysisUrl, + parseRequestUrl, prepareError, slash, } from './plugins/vite-utils' @@ -1056,7 +1057,11 @@ export default function vitePluginRsc( } server.middlewares.use(async (req, res, next) => { - const url = new URL(req.url ?? '/', `http://localhost`) + const url = parseRequestUrl(req.url) + if (!url) { + next() + return + } if (url.pathname === '/__vite_rsc_load_module_dev_proxy') { try { const handler = await createHandler(url) diff --git a/packages/plugin-rsc/src/plugins/find-source-map-url.ts b/packages/plugin-rsc/src/plugins/find-source-map-url.ts index 4f84bc63c..fa77bff26 100644 --- a/packages/plugin-rsc/src/plugins/find-source-map-url.ts +++ b/packages/plugin-rsc/src/plugins/find-source-map-url.ts @@ -6,7 +6,7 @@ import { type Plugin, type ViteDevServer, } from 'vite' -import { slash } from './vite-utils' +import { parseRequestUrl, slash } from './vite-utils' // // support findSourceMapURL @@ -21,7 +21,11 @@ export function vitePluginFindSourceMapURL(): Plugin[] { apply: 'serve', configureServer(server) { server.middlewares.use(async (req, res, next) => { - const url = new URL(req.url!, `http://localhost`) + const url = parseRequestUrl(req.url) + if (!url) { + next() + return + } if (url.pathname === '/__vite_rsc_findSourceMapURL') { let filename = url.searchParams.get('filename')! let environmentName = url.searchParams.get('environmentName')! diff --git a/packages/plugin-rsc/src/plugins/vite-utils.test.ts b/packages/plugin-rsc/src/plugins/vite-utils.test.ts new file mode 100644 index 000000000..133243a72 --- /dev/null +++ b/packages/plugin-rsc/src/plugins/vite-utils.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, it } from 'vitest' +import { parseRequestUrl } from './vite-utils' + +describe(parseRequestUrl, () => { + it('parses normal request urls', () => { + expect(parseRequestUrl('/')?.pathname).toBe('/') + expect( + parseRequestUrl('/__vite_rsc_findSourceMapURL?filename=a')?.pathname, + ).toBe('/__vite_rsc_findSourceMapURL') + expect(parseRequestUrl(undefined)?.pathname).toBe('/') + }) + + it('returns undefined instead of throwing for unparseable request urls', () => { + // A request to `//` is valid HTTP but `new URL('//', base)` throws + // ERR_INVALID_URL. An uncaught throw inside an async Connect middleware + // becomes an unhandled rejection and kills the dev server, so the + // middleware skips to `next()` on undefined instead. + expect(parseRequestUrl('//')).toBeUndefined() + expect(parseRequestUrl('//[')).toBeUndefined() + }) +}) diff --git a/packages/plugin-rsc/src/plugins/vite-utils.ts b/packages/plugin-rsc/src/plugins/vite-utils.ts index 1ebe38372..a5e11e060 100644 --- a/packages/plugin-rsc/src/plugins/vite-utils.ts +++ b/packages/plugin-rsc/src/plugins/vite-utils.ts @@ -165,3 +165,15 @@ export function evalValue(rawValue: string): T { // https://github.com/vitejs/vite/blob/84079a84ad94de4c1ef4f1bdb2ab448ff2c01196/packages/vite/src/node/utils.ts#L321 export const directRequestRE: RegExp = /(\?|&)direct=?(?:&|$)/ + +// Parse an incoming dev-middleware request URL without throwing. Requests +// like `//` or `//[` are valid HTTP but do not parse as URLs; an uncaught +// throw inside an async Connect middleware becomes an unhandled rejection +// and takes down the whole dev server. Callers skip to `next()` on undefined. +export function parseRequestUrl(reqUrl: string | undefined): URL | undefined { + try { + return new URL(reqUrl ?? '/', 'http://localhost') + } catch { + return undefined + } +}