diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts index f5c9c8f29..27f4a40e4 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 + } +}