diff --git a/src/router.ts b/src/router.ts index c554f031..37312be2 100644 --- a/src/router.ts +++ b/src/router.ts @@ -1,4 +1,8 @@ -import { createRouter as _createRouter } from "radix3"; +import { + createRouter as _createRouter, + toRouteMatcher, + RouteMatcher, +} from "radix3"; import type { HTTPMethod, EventHandler } from "./types"; import { createError } from "./error"; import { eventHandler, toEventHandler } from "./event"; @@ -44,6 +48,8 @@ export function createRouter(opts: CreateRouterOptions = {}): Router { const _router = _createRouter({}); const routes: Record = {}; + let _matcher: RouteMatcher | undefined; + const router: Router = {} as Router; // Utilities to add a new route @@ -100,7 +106,32 @@ export function createRouter(opts: CreateRouterOptions = {}): Router { const method = ( event.node.req.method || "get" ).toLowerCase() as RouterMethod; - const handler = matched.handlers[method] || matched.handlers.all; + + let handler: EventHandler | undefined = + matched.handlers[method] || matched.handlers.all; + + // Fallback to search for shadowed routes + if (!handler) { + if (!_matcher) { + _matcher = toRouteMatcher(_router); + } + // Default order is less specific to most specific + const _matches = _matcher.matchAll(path).reverse() as RouteNode[]; + for (const _match of _matches) { + if (_match.handlers[method]) { + handler = _match.handlers[method]; + matched.handlers[method] = matched.handlers[method] || handler; + break; + } + if (_match.handlers.all) { + handler = _match.handlers.all; + matched.handlers.all = matched.handlers.all || handler; + break; + } + } + } + + // Method not matched if (!handler) { if (opts.preemptive || opts.preemtive) { throw createError({ diff --git a/test/router.test.ts b/test/router.test.ts index 6a1f9975..566c4d60 100644 --- a/test/router.test.ts +++ b/test/router.test.ts @@ -97,6 +97,29 @@ describe("router", () => { const res = await request.get("/404"); expect(res.status).toEqual(404); }); + + it("Handle shadowed route", async () => { + router.post( + "/test/123", + eventHandler((event) => `[${event.method}] ${event.path}`) + ); + + router.use( + "/test/**", + eventHandler((event) => `[${event.method}] ${event.path}`) + ); + + // Loop to validate cached behavior + for (let i = 0; i < 5; i++) { + const postRed = await request.post("/test/123"); + expect(postRed.status).toEqual(200); + expect(postRed.text).toEqual("[POST] /test/123"); + + const getRes = await request.get("/test/123"); + expect(getRes.status).toEqual(200); + expect(getRes.text).toEqual("[GET] /test/123"); + } + }); }); describe("router (preemptive)", () => {