Skip to content

fix(server): guard decodeURIComponent on the non-bundle indexHtml path - #22789

Closed
mahirhir wants to merge 1 commit into
vitejs:mainfrom
mahirhir:greymoth-jp/fix/indexhtml-malformed-uri-dev-path
Closed

mahirhir wants to merge 1 commit into
vitejs:mainfrom
mahirhir:greymoth-jp/fix/indexhtml-malformed-uri-dev-path

Conversation

@mahirhir

Copy link
Copy Markdown
Contributor

What

The non-fullBundle branch of indexHtmlMiddleware called decodeURIComponent(url) unguarded in two places (packages/vite/src/node/server/middlewares/indexHtml.ts). A request for a malformed .html URL such as GET /%c0.html passes rejectInvalidRequest (which only blocks #), passes the fullBundle guard (which calls next() immediately for out-of-scope files), and then lands on the unguarded decode — throwing URIError: URI malformed and crashing the middleware.

Relation to #22781

#22781 (my previous contribution, merged by @sapphi-red) added exactly this guard to the fullBundle branch:

// fullBundle path — already guarded (#22781)
let pathname
try {
  pathname = decodeURIComponent(url)
} catch {
  return next()
}

This PR mirrors that identical pattern onto the regular dev-server (non-fullBundle) path, which is the symmetric sibling left unguarded:

// non-fullBundle path — this PR
let pathname: string
try {
  pathname = decodeURIComponent(url)
} catch {
  // ignore malformed URI
  return next()
}

let filePath: string
if (isDev && url.startsWith(FS_PREFIX)) {
  filePath = fsPathFromId(pathname)
} else {
  filePath = normalizePath(path.resolve(path.join(root, pathname)))
}

decodeURIComponent(fsPathFromId(url)) and fsPathFromId(decodeURIComponent(url)) are equivalent for any URI where FS_PREFIX (/@fs/) is not itself percent-encoded (it cannot be, since it comes from cleanUrl). Valid input is byte-identical to the previous behaviour; only the throwing path changes.

Test

Added a test in packages/vite/src/node/server/middlewares/__tests__/indexHtml.spec.ts that wraps the Vite middleware stack in a real HTTP server and asserts that GET /%c0.html returns a non-500 response (falls through to 404 rather than crashing).


AI-assist disclosure: this patch was drafted with Claude Code assistance. The logic, file identification, and guard pattern are derived directly from the already-merged #22781; no novel design decisions were required.

The non-`fullBundle` branch of `indexHtmlMiddleware` called
`decodeURIComponent(url)` (and `decodeURIComponent(fsPathFromId(url))`)
without a guard.  A request for a malformed URL such as `GET /%c0.html`
therefore threw `URIError: URI malformed`, crashing the middleware
instead of falling through.

#22781 already added the identical guard to the `fullBundle` branch.
This commit mirrors that pattern to the regular dev-server path: decode
once inside a try/catch, call `return next()` on failure, and use the
decoded value (`pathname`) in both the `/@fs/` and the root branches.

Valid input is byte-identical to the previous behaviour; only the
throwing path changes.
@sapphi-red sapphi-red added p2-edge-case Bug, but has workaround or limited in scope (priority) feat: dev dev server labels Jun 30, 2026
@mahirhir

mahirhir commented Jul 4, 2026

Copy link
Copy Markdown
Contributor Author

Closing this since CI is red and I can't verify it properly. I'd rather not leave a broken PR in your queue. Sorry for the noise.

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

Labels

feat: dev dev server p2-edge-case Bug, but has workaround or limited in scope (priority)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants