diff --git a/packages/fresh/src/router.ts b/packages/fresh/src/router.ts index b731f7b9ce2..d32a1fb68e6 100644 --- a/packages/fresh/src/router.ts +++ b/packages/fresh/src/router.ts @@ -116,9 +116,26 @@ export class UrlPatternRouter implements Router { pattern: null, }; - const staticMatch = this.#statics.get(url.pathname); + let pathname = url.pathname; + let staticMatch = this.#statics.get(pathname); + + // Try alternate trailing slash form if no exact match found. + // Routes may be registered with or without trailing slashes, + // and requests may arrive in either form (e.g. when using + // trailingSlashes("always")). + if (staticMatch === undefined && pathname !== "/") { + const alt = pathname.endsWith("/") + ? pathname.slice(0, -1) + : pathname + "/"; + const altMatch = this.#statics.get(alt); + if (altMatch !== undefined) { + staticMatch = altMatch; + pathname = alt; + } + } + if (staticMatch !== undefined) { - result.pattern = url.pathname; + result.pattern = pathname; let handlers = staticMatch.byMethod[method]; if (method === "HEAD" && handlers.length === 0) { diff --git a/packages/fresh/src/router_test.ts b/packages/fresh/src/router_test.ts index ae082a43b6a..1776754173b 100644 --- a/packages/fresh/src/router_test.ts +++ b/packages/fresh/src/router_test.ts @@ -70,6 +70,78 @@ Deno.test("UrlPatternRouter - wrong + correct method", () => { }); }); +Deno.test("UrlPatternRouter - trailing slash matches route without slash", () => { + const router = new UrlPatternRouter(); + const A = () => {}; + router.add("GET", "/wissen", [A]); + + const res = router.match("GET", new URL("/wissen/", "http://localhost")); + expect(res).toEqual({ + params: Object.create(null), + handlers: [A], + methodMatch: true, + pattern: "/wissen", + }); +}); + +Deno.test("UrlPatternRouter - no trailing slash matches route with slash", () => { + const router = new UrlPatternRouter(); + const A = () => {}; + router.add("GET", "/wissen/", [A]); + + const res = router.match("GET", new URL("/wissen", "http://localhost")); + expect(res).toEqual({ + params: Object.create(null), + handlers: [A], + methodMatch: true, + pattern: "/wissen/", + }); +}); + +Deno.test("UrlPatternRouter - exact match takes priority over trailing slash fallback", () => { + const router = new UrlPatternRouter(); + const A = () => {}; + const B = () => {}; + router.add("GET", "/wissen", [A]); + router.add("GET", "/wissen/", [B]); + + const withSlash = router.match( + "GET", + new URL("/wissen/", "http://localhost"), + ); + expect(withSlash).toEqual({ + params: Object.create(null), + handlers: [B], + methodMatch: true, + pattern: "/wissen/", + }); + + const withoutSlash = router.match( + "GET", + new URL("/wissen", "http://localhost"), + ); + expect(withoutSlash).toEqual({ + params: Object.create(null), + handlers: [A], + methodMatch: true, + pattern: "/wissen", + }); +}); + +Deno.test("UrlPatternRouter - root trailing slash does not double-match", () => { + const router = new UrlPatternRouter(); + const A = () => {}; + router.add("GET", "/", [A]); + + const res = router.match("GET", new URL("/", "http://localhost")); + expect(res).toEqual({ + params: Object.create(null), + handlers: [A], + methodMatch: true, + pattern: "/", + }); +}); + Deno.test("UrlPatternRouter - convert patterns automatically", () => { const router = new UrlPatternRouter(); const A = () => {};