diff --git a/packages/playground/fs-serve/__tests__/fs-serve.spec.ts b/packages/playground/fs-serve/__tests__/fs-serve.spec.ts
index c618186b9bcd64..eba1e441881710 100644
--- a/packages/playground/fs-serve/__tests__/fs-serve.spec.ts
+++ b/packages/playground/fs-serve/__tests__/fs-serve.spec.ts
@@ -23,6 +23,15 @@ describe('main', () => {
expect(await page.textContent('.safe-fetch-status')).toBe('200')
})
+ test('safe fetch with special characters', async () => {
+ expect(
+ await page.textContent('.safe-fetch-subdir-special-characters')
+ ).toMatch('KEY=safe')
+ expect(
+ await page.textContent('.safe-fetch-subdir-special-characters-status')
+ ).toBe('200')
+ })
+
test('unsafe fetch', async () => {
expect(await page.textContent('.unsafe-fetch')).toMatch('403 Restricted')
expect(await page.textContent('.unsafe-fetch-status')).toBe('403')
@@ -33,6 +42,13 @@ describe('main', () => {
expect(await page.textContent('.safe-fs-fetch-status')).toBe('200')
})
+ test('safe fs fetch with special characters', async () => {
+ expect(await page.textContent('.safe-fs-fetch-special-characters')).toBe(
+ stringified
+ )
+ expect(await page.textContent('.safe-fs-fetch-status')).toBe('200')
+ })
+
test('unsafe fs fetch', async () => {
expect(await page.textContent('.unsafe-fs-fetch')).toBe('')
expect(await page.textContent('.unsafe-fs-fetch-status')).toBe('403')
diff --git a/packages/playground/fs-serve/root/src/index.html b/packages/playground/fs-serve/root/src/index.html
index 9e4f728a593a91..951e14ad2cce91 100644
--- a/packages/playground/fs-serve/root/src/index.html
+++ b/packages/playground/fs-serve/root/src/index.html
@@ -11,6 +11,8 @@
Safe Fetch
Safe Fetch Subdirectory
+
+
Unsafe Fetch
@@ -19,6 +21,8 @@ Unsafe Fetch
Safe /@fs/ Fetch
+
+
Unsafe /@fs/ Fetch
@@ -56,6 +60,16 @@ Denied
text('.safe-fetch-subdir', JSON.stringify(data))
})
+ // inside allowed dir, with special characters, safe fetch
+ fetch('/src/special%20characters%20%C3%A5%C3%A4%C3%B6/safe.txt')
+ .then((r) => {
+ text('.safe-fetch-subdir-special-characters-status', r.status)
+ return r.text()
+ })
+ .then((data) => {
+ text('.safe-fetch-subdir-special-characters', JSON.stringify(data))
+ })
+
// outside of allowed dir, treated as unsafe
fetch('/unsafe.txt')
.then((r) => {
@@ -92,6 +106,20 @@ Denied
console.error(e)
})
+ // not imported before, inside root with special characters, treated as safe
+ fetch(
+ '/@fs/' +
+ ROOT +
+ '/root/src/special%20characters%20%C3%A5%C3%A4%C3%B6/safe.json'
+ )
+ .then((r) => {
+ text('.safe-fs-fetch-special-characters-status', r.status)
+ return r.json()
+ })
+ .then((data) => {
+ text('.safe-fs-fetch-special-characters', JSON.stringify(data))
+ })
+
// .env, denied by default
fetch('/@fs/' + ROOT + '/root/.env')
.then((r) => {
diff --git "a/packages/playground/fs-serve/root/src/special characters \303\245\303\244\303\266/safe.json" "b/packages/playground/fs-serve/root/src/special characters \303\245\303\244\303\266/safe.json"
new file mode 100644
index 00000000000000..84f96593c10bad
--- /dev/null
+++ "b/packages/playground/fs-serve/root/src/special characters \303\245\303\244\303\266/safe.json"
@@ -0,0 +1,3 @@
+{
+ "msg": "safe"
+}
diff --git "a/packages/playground/fs-serve/root/src/special characters \303\245\303\244\303\266/safe.txt" "b/packages/playground/fs-serve/root/src/special characters \303\245\303\244\303\266/safe.txt"
new file mode 100644
index 00000000000000..3f3d0607101642
--- /dev/null
+++ "b/packages/playground/fs-serve/root/src/special characters \303\245\303\244\303\266/safe.txt"
@@ -0,0 +1 @@
+KEY=safe
diff --git a/packages/vite/src/node/server/middlewares/static.ts b/packages/vite/src/node/server/middlewares/static.ts
index a6623338783cc8..5fb4f7fad2e055 100644
--- a/packages/vite/src/node/server/middlewares/static.ts
+++ b/packages/vite/src/node/server/middlewares/static.ts
@@ -111,7 +111,7 @@ export function serveRawFsMiddleware(
// Keep the named function. The name is visible in debug logs via `DEBUG=connect:dispatcher ...`
return function viteServeRawFsMiddleware(req, res, next) {
- let url = req.url!
+ let url = decodeURI(req.url!)
// In some cases (e.g. linked monorepos) files outside of root will
// reference assets that are also out of served root. In such cases
// the paths are rewritten to `/@fs/` prefixed paths and must be served by