diff --git a/packages/fresh/src/middlewares/static_files.ts b/packages/fresh/src/middlewares/static_files.ts index ee06b0f7e3f..288f9a98eb6 100644 --- a/packages/fresh/src/middlewares/static_files.ts +++ b/packages/fresh/src/middlewares/static_files.ts @@ -4,6 +4,14 @@ import { BUILD_ID } from "@fresh/build-id"; import { tracer } from "../otel.ts"; import { getBuildCache } from "../context.ts"; +/** Decode and re-encode each path segment so that characters like commas + * are percent-encoded consistently with how `prepareStaticFile` stores + * entries in the build cache. */ +function normalizePathname(pathname: string): string { + return "/" + + pathname.split("/").filter(Boolean).map(encodeURIComponent).join("/"); +} + /** * Fresh middleware to serve static files from the `static/` directory. * ```ts @@ -25,6 +33,13 @@ export function staticFiles(): Middleware { : "/"; } + try { + pathname = normalizePathname(decodeURIComponent(pathname)); + } catch (_e: unknown) { + if (!(_e instanceof URIError)) throw _e; + return await ctx.next(); + } + // Fast path bail out const startTime = performance.now() + performance.timeOrigin; const file = await buildCache.readFile(pathname); diff --git a/packages/fresh/src/middlewares/static_files_test.ts b/packages/fresh/src/middlewares/static_files_test.ts index 3ba5daacb51..3c0b311c74c 100644 --- a/packages/fresh/src/middlewares/static_files_test.ts +++ b/packages/fresh/src/middlewares/static_files_test.ts @@ -296,3 +296,29 @@ Deno.test("static files - fallthrough", async () => { expect(text).toEqual("it works"); expect(called).toEqual(["before", "after"]); }); + +Deno.test("static files - comma in pathname", async () => { + const key = systemPathToUrlEncoded("foo,bar.css"); + + const buildCache = new MockBuildCache({ + [key]: { + content: "body {}", + hash: null, + }, + }); + + const server = serveMiddleware( + staticFiles(), + { buildCache }, + ); + + // Browser/request path usually keeps the comma unencoded + let res = await server.get("/foo,bar.css"); + await res.body?.cancel(); + expect(res.status).toEqual(200); + + // Encoded form should also resolve + res = await server.get("/foo%2Cbar.css"); + await res.body?.cancel(); + expect(res.status).toEqual(200); +});