From 761f55fe4fc62c787258339192df289f25213b3e Mon Sep 17 00:00:00 2001 From: Hans Ott Date: Wed, 26 Nov 2025 10:16:27 +0100 Subject: [PATCH 1/4] POC for custom route patterns --- library/helpers/buildRouteFromURL.test.ts | 12 ++++++ library/helpers/buildRouteFromURL.ts | 49 ++++++++++++++++++++++- 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/library/helpers/buildRouteFromURL.test.ts b/library/helpers/buildRouteFromURL.test.ts index 948333100..a70f1a902 100644 --- a/library/helpers/buildRouteFromURL.test.ts +++ b/library/helpers/buildRouteFromURL.test.ts @@ -198,3 +198,15 @@ t.test("it detects numeric comma separated arrays", async (t) => { t.same(buildRouteFromURL("/users/1,2,3_"), "/users/1,2,3_"); t.same(buildRouteFromURL("/users/1,2,3a"), "/users/1,2,3a"); }); + +t.test("it supports custom patterns", async () => { + t.same( + buildRouteFromURL("/prefix-103799/api/dashboard", ["prefix-{digits}"]), + "/:custom/api/dashboard" + ); + + t.same( + buildRouteFromURL("/blog/01-31513/slug", ["{digits}-{digits}"]), + "/blog/:custom/slug" + ); +}); diff --git a/library/helpers/buildRouteFromURL.ts b/library/helpers/buildRouteFromURL.ts index 44fc8bb24..23b23bd32 100644 --- a/library/helpers/buildRouteFromURL.ts +++ b/library/helpers/buildRouteFromURL.ts @@ -1,3 +1,5 @@ +import { safeCreateRegExp } from "../agent/safeCreateRegExp"; +import { escapeStringRegexp } from "./escapeStringRegexp"; import { looksLikeASecret } from "./looksLikeASecret"; import { safeDecodeURIComponent } from "./safeDecodeURIComponent"; import { tryParseURLPath } from "./tryParseURLPath"; @@ -15,7 +17,7 @@ const HASH = /^(?:[a-f0-9]{32}|[a-f0-9]{40}|[a-f0-9]{64}|[a-f0-9]{128})$/i; const HASH_LENGTHS = [32, 40, 64, 128]; const NUMBER_ARRAY = /^\d+(?:,\d+)*$/; -export function buildRouteFromURL(url: string) { +export function buildRouteFromURL(url: string, custom: string[] = []) { let path = tryParseURLPath(url); if (!path) { @@ -29,7 +31,10 @@ export function buildRouteFromURL(url: string) { } } - const route = path.split("/").map(replaceURLSegmentWithParam).join("/"); + const route = path + .split("/") + .map(replaceURLSegmentWithCustomParam(custom)) + .join("/"); if (route === "/") { return "/"; @@ -42,6 +47,46 @@ export function buildRouteFromURL(url: string) { return route; } +function compileCustom(pattern: string) { + if (!pattern.includes("{") || !pattern.includes("}")) { + return undefined; + } + + const supported: Record = { + "{digits}": `\\d+`, + "{alpha}": "[a-zA-Z]+", + }; + + // Split the pattern into tokens (placeholders and literals) + const placeholderRegex = /(\{[a-zA-Z]+})/g; + const parts = pattern.split(placeholderRegex); + const regexParts = parts.map((part) => { + if (supported[part]) { + return supported[part]; + } + + return escapeStringRegexp(part); + }); + + return safeCreateRegExp(`^${regexParts.join("")}$`, ""); +} + +function replaceURLSegmentWithCustomParam(custom: string[]) { + const customPatterns = custom + .map(compileCustom) + .filter((p) => p !== undefined); + + return (segment: string) => { + for (const pattern of customPatterns) { + if (pattern && pattern.test(segment)) { + return `:custom`; + } + } + + return replaceURLSegmentWithParam(segment); + }; +} + function replaceURLSegmentWithParam(segment: string) { const charCode = segment.charCodeAt(0); const startsWithNumber = charCode >= 48 && charCode <= 57; // ASCII codes for '0' to '9' From 535ee83994314e7b6adffc7a4fdbe48937fda081 Mon Sep 17 00:00:00 2001 From: Hans Ott Date: Wed, 26 Nov 2025 10:21:22 +0100 Subject: [PATCH 2/4] Simplify --- library/helpers/buildRouteFromURL.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/library/helpers/buildRouteFromURL.ts b/library/helpers/buildRouteFromURL.ts index 23b23bd32..ecd61d789 100644 --- a/library/helpers/buildRouteFromURL.ts +++ b/library/helpers/buildRouteFromURL.ts @@ -57,7 +57,6 @@ function compileCustom(pattern: string) { "{alpha}": "[a-zA-Z]+", }; - // Split the pattern into tokens (placeholders and literals) const placeholderRegex = /(\{[a-zA-Z]+})/g; const parts = pattern.split(placeholderRegex); const regexParts = parts.map((part) => { @@ -78,7 +77,7 @@ function replaceURLSegmentWithCustomParam(custom: string[]) { return (segment: string) => { for (const pattern of customPatterns) { - if (pattern && pattern.test(segment)) { + if (pattern.test(segment)) { return `:custom`; } } From 5f10cfdba724e0206c014480fde1995fe6ae6233 Mon Sep 17 00:00:00 2001 From: Hans Ott Date: Wed, 26 Nov 2025 10:58:37 +0100 Subject: [PATCH 3/4] Add `addRouteParam` function --- library/agent/addRouteParam.test.ts | 43 ++++++ library/agent/addRouteParam.ts | 38 ++++++ library/helpers/buildRouteFromURL.test.ts | 128 ++++++++++-------- library/helpers/buildRouteFromURL.ts | 30 ++-- library/helpers/matchEndpoints.test.ts | 4 +- library/index.ts | 3 + library/sources/FunctionsFramework.ts | 3 +- library/sources/express/contextFromRequest.ts | 3 +- library/sources/fastify/contextFromRequest.ts | 3 +- library/sources/hapi/contextFromRequest.ts | 3 +- library/sources/hono/contextFromRequest.ts | 3 +- .../sources/http-server/contextFromRequest.ts | 5 +- .../http-server/http2/contextFromStream.ts | 3 +- library/sources/koa/contextFromRequest.ts | 3 +- library/sources/restify/contextFromRequest.ts | 3 +- 15 files changed, 191 insertions(+), 84 deletions(-) create mode 100644 library/agent/addRouteParam.test.ts create mode 100644 library/agent/addRouteParam.ts diff --git a/library/agent/addRouteParam.test.ts b/library/agent/addRouteParam.test.ts new file mode 100644 index 000000000..d8cf40382 --- /dev/null +++ b/library/agent/addRouteParam.test.ts @@ -0,0 +1,43 @@ +import * as t from "tap"; +import { wrap } from "../helpers/wrap"; +import { addRouteParam, getRegisteredRouteParams } from "./addRouteParam"; + +let logs: string[] = []; +wrap(console, "warn", function warn() { + return function warn(message: string) { + logs.push(message); + }; +}); + +t.beforeEach(() => { + logs = []; +}); + +t.test("it warns if slash is included", async (t) => { + addRouteParam("prefix/{digits}"); + t.same(logs[0], "addRouteParam(...) expects a pattern without slashes."); +}); + +t.test("it warns if no curly braces are included", async (t) => { + addRouteParam("prefix-digits"); + t.same( + logs[0], + "addRouteParam(...) expects a pattern that includes {digits} or {alpha}." + ); +}); + +t.test("addRouteParam adds valid patterns", async (t) => { + addRouteParam("prefix-{digits}"); + t.same(getRegisteredRouteParams().length, 1); + t.same(getRegisteredRouteParams()[0].test("prefix-12345"), true); + + addRouteParam("prefix-{digits}"); + t.same(getRegisteredRouteParams().length, 1); + + addRouteParam("prefix-{alpha}"); + t.same(getRegisteredRouteParams().length, 2); + t.same(getRegisteredRouteParams()[1].test("prefix-abcde"), true); + + addRouteParam("prefix-{alpha}"); + t.same(getRegisteredRouteParams().length, 2); +}); diff --git a/library/agent/addRouteParam.ts b/library/agent/addRouteParam.ts new file mode 100644 index 000000000..ce2cdc194 --- /dev/null +++ b/library/agent/addRouteParam.ts @@ -0,0 +1,38 @@ +// oxlint-disable no-console +import { compileCustomPattern } from "../helpers/buildRouteFromURL"; + +const registeredPatterns: RegExp[] = []; +const registeredPatternsSet: Set = new Set(); + +export function addRouteParam(pattern: string) { + if (!pattern.includes("{") || !pattern.includes("}")) { + console.warn( + "addRouteParam(...) expects a pattern that includes {digits} or {alpha}." + ); + return; + } + + if (pattern.includes("/")) { + console.warn("addRouteParam(...) expects a pattern without slashes."); + return; + } + + const regex = compileCustomPattern(pattern); + if (!regex) { + console.warn( + "addRouteParam(...) could not compile the provided pattern into a valid regular expression." + ); + return; + } + + if (registeredPatternsSet.has(pattern)) { + return; + } + + registeredPatternsSet.add(pattern); + registeredPatterns.push(regex); +} + +export function getRegisteredRouteParams(): RegExp[] { + return registeredPatterns; +} diff --git a/library/helpers/buildRouteFromURL.test.ts b/library/helpers/buildRouteFromURL.test.ts index a70f1a902..8066f5d89 100644 --- a/library/helpers/buildRouteFromURL.test.ts +++ b/library/helpers/buildRouteFromURL.test.ts @@ -1,128 +1,134 @@ import * as t from "tap"; -import { buildRouteFromURL } from "./buildRouteFromURL"; +import { buildRouteFromURL, compileCustomPattern } from "./buildRouteFromURL"; import * as ObjectID from "bson-objectid"; import { createHash } from "crypto"; t.test("it returns undefined for invalid URLs", async () => { - t.same(buildRouteFromURL(""), undefined); - t.same(buildRouteFromURL("http"), undefined); + t.same(buildRouteFromURL("", []), undefined); + t.same(buildRouteFromURL("http", []), undefined); }); t.test("it returns / for root URLs", async () => { - t.same(buildRouteFromURL("/"), "/"); - t.same(buildRouteFromURL("http://localhost/"), "/"); + t.same(buildRouteFromURL("/", []), "/"); + t.same(buildRouteFromURL("http://localhost/", []), "/"); }); t.test("it replaces numbers", async () => { - t.same(buildRouteFromURL("/posts/3"), "/posts/:number"); - t.same(buildRouteFromURL("http://localhost/posts/3"), "/posts/:number"); - t.same(buildRouteFromURL("http://localhost/posts/3/"), "/posts/:number"); + t.same(buildRouteFromURL("/posts/3", []), "/posts/:number"); + t.same(buildRouteFromURL("http://localhost/posts/3", []), "/posts/:number"); + t.same(buildRouteFromURL("http://localhost/posts/3/", []), "/posts/:number"); t.same( - buildRouteFromURL("http://localhost/posts/3/comments/10"), + buildRouteFromURL("http://localhost/posts/3/comments/10", []), "/posts/:number/comments/:number" ); t.same( - buildRouteFromURL("/blog/2023/05/great-article"), + buildRouteFromURL("/blog/2023/05/great-article", []), "/blog/:number/:number/great-article" ); }); t.test("it replaces dates", async () => { - t.same(buildRouteFromURL("/posts/2023-05-01"), "/posts/:date"); - t.same(buildRouteFromURL("/posts/2023-05-01/"), "/posts/:date"); + t.same(buildRouteFromURL("/posts/2023-05-01", []), "/posts/:date"); + t.same(buildRouteFromURL("/posts/2023-05-01/", []), "/posts/:date"); t.same( - buildRouteFromURL("/posts/2023-05-01/comments/2023-05-01"), + buildRouteFromURL("/posts/2023-05-01/comments/2023-05-01", []), "/posts/:date/comments/:date" ); - t.same(buildRouteFromURL("/posts/01-05-2023"), "/posts/:date"); + t.same(buildRouteFromURL("/posts/01-05-2023", []), "/posts/:date"); }); t.test("it ignores API version numbers", async () => { - t.same(buildRouteFromURL("/v1/posts/3"), "/v1/posts/:number"); + t.same(buildRouteFromURL("/v1/posts/3", []), "/v1/posts/:number"); }); t.test("it replaces UUIDs v1", async () => { t.same( - buildRouteFromURL("/posts/d9428888-122b-11e1-b85c-61cd3cbb3210"), + buildRouteFromURL("/posts/d9428888-122b-11e1-b85c-61cd3cbb3210", []), "/posts/:uuid" ); }); t.test("it replaces UUIDs v2", async () => { t.same( - buildRouteFromURL("/posts/000003e8-2363-21ef-b200-325096b39f47"), + buildRouteFromURL("/posts/000003e8-2363-21ef-b200-325096b39f47", []), "/posts/:uuid" ); }); t.test("it replaces UUIDs v3", async () => { t.same( - buildRouteFromURL("/posts/a981a0c2-68b1-35dc-bcfc-296e52ab01ec"), + buildRouteFromURL("/posts/a981a0c2-68b1-35dc-bcfc-296e52ab01ec", []), "/posts/:uuid" ); }); t.test("it replaces UUIDs v4", async () => { t.same( - buildRouteFromURL("/posts/109156be-c4fb-41ea-b1b4-efe1671c5836"), + buildRouteFromURL("/posts/109156be-c4fb-41ea-b1b4-efe1671c5836", []), "/posts/:uuid" ); }); t.test("it replaces UUIDs v5", async () => { t.same( - buildRouteFromURL("/posts/90123e1c-7512-523e-bb28-76fab9f2f73d"), + buildRouteFromURL("/posts/90123e1c-7512-523e-bb28-76fab9f2f73d", []), "/posts/:uuid" ); }); t.test("it replaces UUIDs v6", async () => { t.same( - buildRouteFromURL("/posts/1ef21d2f-1207-6660-8c4f-419efbd44d48"), + buildRouteFromURL("/posts/1ef21d2f-1207-6660-8c4f-419efbd44d48", []), "/posts/:uuid" ); }); t.test("it replaces UUIDs v7", async () => { t.same( - buildRouteFromURL("/posts/017f22e2-79b0-7cc3-98c4-dc0c0c07398f"), + buildRouteFromURL("/posts/017f22e2-79b0-7cc3-98c4-dc0c0c07398f", []), "/posts/:uuid" ); }); t.test("it replaces UUIDs v8", async () => { t.same( - buildRouteFromURL("/posts/0d8f23a0-697f-83ae-802e-48f3756dd581"), + buildRouteFromURL("/posts/0d8f23a0-697f-83ae-802e-48f3756dd581", []), "/posts/:uuid" ); }); t.test("it ignores invalid UUIDs", async () => { t.same( - buildRouteFromURL("/posts/00000000-0000-1000-6000-000000000000"), + buildRouteFromURL("/posts/00000000-0000-1000-6000-000000000000", []), "/posts/00000000-0000-1000-6000-000000000000" ); }); t.test("it ignores strings", async () => { - t.same(buildRouteFromURL("/posts/abc"), "/posts/abc"); + t.same(buildRouteFromURL("/posts/abc", []), "/posts/abc"); }); t.test("it replaces email addresses", async () => { - t.same(buildRouteFromURL("/login/john.doe@acme.com"), "/login/:email"); - t.same(buildRouteFromURL("/login/john.doe+alias@acme.com"), "/login/:email"); + t.same(buildRouteFromURL("/login/john.doe@acme.com", []), "/login/:email"); + t.same( + buildRouteFromURL("/login/john.doe+alias@acme.com", []), + "/login/:email" + ); }); t.test("it replaces IP addresses", async () => { - t.same(buildRouteFromURL("/block/1.2.3.4"), "/block/:ip"); + t.same(buildRouteFromURL("/block/1.2.3.4", []), "/block/:ip"); t.same( - buildRouteFromURL("/block/2001:2:ffff:ffff:ffff:ffff:ffff:ffff"), + buildRouteFromURL("/block/2001:2:ffff:ffff:ffff:ffff:ffff:ffff", []), "/block/:ip" ); - t.same(buildRouteFromURL("/block/64:ff9a::255.255.255.255"), "/block/:ip"); - t.same(buildRouteFromURL("/block/100::"), "/block/:ip"); - t.same(buildRouteFromURL("/block/fec0::"), "/block/:ip"); - t.same(buildRouteFromURL("/block/227.202.96.196"), "/block/:ip"); + t.same( + buildRouteFromURL("/block/64:ff9a::255.255.255.255", []), + "/block/:ip" + ); + t.same(buildRouteFromURL("/block/100::", []), "/block/:ip"); + t.same(buildRouteFromURL("/block/fec0::", []), "/block/:ip"); + t.same(buildRouteFromURL("/block/227.202.96.196", []), "/block/:ip"); }); function generateHash(type: string) { @@ -130,15 +136,27 @@ function generateHash(type: string) { } t.test("it replaces hashes", async () => { - t.same(buildRouteFromURL(`/files/${generateHash("md5")}`), "/files/:hash"); - t.same(buildRouteFromURL(`/files/${generateHash("sha1")}`), "/files/:hash"); - t.same(buildRouteFromURL(`/files/${generateHash("sha256")}`), "/files/:hash"); - t.same(buildRouteFromURL(`/files/${generateHash("sha512")}`), "/files/:hash"); + t.same( + buildRouteFromURL(`/files/${generateHash("md5")}`, []), + "/files/:hash" + ); + t.same( + buildRouteFromURL(`/files/${generateHash("sha1")}`, []), + "/files/:hash" + ); + t.same( + buildRouteFromURL(`/files/${generateHash("sha256")}`, []), + "/files/:hash" + ); + t.same( + buildRouteFromURL(`/files/${generateHash("sha512")}`, []), + "/files/:hash" + ); }); t.test("it replaces secrets", async () => { t.same( - buildRouteFromURL("/confirm/CnJ4DunhYfv2db6T1FRfciRBHtlNKOYrjoz"), + buildRouteFromURL("/confirm/CnJ4DunhYfv2db6T1FRfciRBHtlNKOYrjoz", []), "/confirm/:secret" ); }); @@ -150,24 +168,24 @@ t.test("it replaces BSON ObjectIDs", async () => { "/posts/:objectId" ); t.same( - buildRouteFromURL(`/posts/66ec29159d00113616fc7184`), + buildRouteFromURL(`/posts/66ec29159d00113616fc7184`, []), "/posts/:objectId" ); }); t.test("it replaces ULID strings", async () => { t.same( - buildRouteFromURL("/posts/01ARZ3NDEKTSV4RRFFQ69G5FAV"), + buildRouteFromURL("/posts/01ARZ3NDEKTSV4RRFFQ69G5FAV", []), "/posts/:ulid" ); t.same( - buildRouteFromURL("/posts/01arz3ndektsv4rrffq69g5fav"), + buildRouteFromURL("/posts/01arz3ndektsv4rrffq69g5fav", []), "/posts/:ulid" ); }); t.test("test_ratelimiting_1 is not a secret", async () => { - t.same(buildRouteFromURL("/test_ratelimiting_1"), "/test_ratelimiting_1"); + t.same(buildRouteFromURL("/test_ratelimiting_1", []), "/test_ratelimiting_1"); }); t.test("it does not detect static files as secrets", async () => { @@ -180,33 +198,37 @@ t.test("it does not detect static files as secrets", async () => { ]; for (const file of files) { - t.same(buildRouteFromURL(`/assets/${file}`), `/assets/${file}`); + t.same(buildRouteFromURL(`/assets/${file}`, []), `/assets/${file}`); } }); t.test("it detects numeric comma separated arrays", async (t) => { - t.same(buildRouteFromURL("/users/1,2"), "/users/:array(number)"); - t.same(buildRouteFromURL("/users/1,2,3,4,5"), "/users/:array(number)"); + t.same(buildRouteFromURL("/users/1,2", []), "/users/:array(number)"); + t.same(buildRouteFromURL("/users/1,2,3,4,5", []), "/users/:array(number)"); t.same( - buildRouteFromURL("/users/100,200,3000000,40000000,500000000"), + buildRouteFromURL("/users/100,200,3000000,40000000,500000000", []), "/users/:array(number)" ); - t.same(buildRouteFromURL("/users/1,2,3,4,"), "/users/1,2,3,4,"); - t.same(buildRouteFromURL("/users/1,"), "/users/1,"); - t.same(buildRouteFromURL("/users/,1,2"), "/users/,1,2"); - t.same(buildRouteFromURL("/users/1,2,3_"), "/users/1,2,3_"); - t.same(buildRouteFromURL("/users/1,2,3a"), "/users/1,2,3a"); + t.same(buildRouteFromURL("/users/1,2,3,4,", []), "/users/1,2,3,4,"); + t.same(buildRouteFromURL("/users/1,", []), "/users/1,"); + t.same(buildRouteFromURL("/users/,1,2", []), "/users/,1,2"); + t.same(buildRouteFromURL("/users/1,2,3_", []), "/users/1,2,3_"); + t.same(buildRouteFromURL("/users/1,2,3a", []), "/users/1,2,3a"); }); t.test("it supports custom patterns", async () => { t.same( - buildRouteFromURL("/prefix-103799/api/dashboard", ["prefix-{digits}"]), + buildRouteFromURL("/prefix-103799/api/dashboard", [ + compileCustomPattern("prefix-{digits}")!, + ]), "/:custom/api/dashboard" ); t.same( - buildRouteFromURL("/blog/01-31513/slug", ["{digits}-{digits}"]), + buildRouteFromURL("/blog/01-31513/slug", [ + compileCustomPattern("{digits}-{digits}")!, + ]), "/blog/:custom/slug" ); }); diff --git a/library/helpers/buildRouteFromURL.ts b/library/helpers/buildRouteFromURL.ts index ecd61d789..9b5d7fb82 100644 --- a/library/helpers/buildRouteFromURL.ts +++ b/library/helpers/buildRouteFromURL.ts @@ -17,7 +17,7 @@ const HASH = /^(?:[a-f0-9]{32}|[a-f0-9]{40}|[a-f0-9]{64}|[a-f0-9]{128})$/i; const HASH_LENGTHS = [32, 40, 64, 128]; const NUMBER_ARRAY = /^\d+(?:,\d+)*$/; -export function buildRouteFromURL(url: string, custom: string[] = []) { +export function buildRouteFromURL(url: string, custom: RegExp[]) { let path = tryParseURLPath(url); if (!path) { @@ -33,7 +33,7 @@ export function buildRouteFromURL(url: string, custom: string[] = []) { const route = path .split("/") - .map(replaceURLSegmentWithCustomParam(custom)) + .map((segment) => replaceURLSegmentWithParam(segment, custom)) .join("/"); if (route === "/") { @@ -47,7 +47,7 @@ export function buildRouteFromURL(url: string, custom: string[] = []) { return route; } -function compileCustom(pattern: string) { +export function compileCustomPattern(pattern: string) { if (!pattern.includes("{") || !pattern.includes("}")) { return undefined; } @@ -70,23 +70,7 @@ function compileCustom(pattern: string) { return safeCreateRegExp(`^${regexParts.join("")}$`, ""); } -function replaceURLSegmentWithCustomParam(custom: string[]) { - const customPatterns = custom - .map(compileCustom) - .filter((p) => p !== undefined); - - return (segment: string) => { - for (const pattern of customPatterns) { - if (pattern.test(segment)) { - return `:custom`; - } - } - - return replaceURLSegmentWithParam(segment); - }; -} - -function replaceURLSegmentWithParam(segment: string) { +function replaceURLSegmentWithParam(segment: string, customPatterns: RegExp[]) { const charCode = segment.charCodeAt(0); const startsWithNumber = charCode >= 48 && charCode <= 57; // ASCII codes for '0' to '9' @@ -130,5 +114,11 @@ function replaceURLSegmentWithParam(segment: string) { return ":secret"; } + for (const pattern of customPatterns) { + if (pattern.test(segment)) { + return `:custom`; + } + } + return segment; } diff --git a/library/helpers/matchEndpoints.test.ts b/library/helpers/matchEndpoints.test.ts index a97070cc8..4afefefab 100644 --- a/library/helpers/matchEndpoints.test.ts +++ b/library/helpers/matchEndpoints.test.ts @@ -14,7 +14,7 @@ const context: Context = { cookies: {}, routeParams: {}, source: "express", - route: buildRouteFromURL(url), + route: buildRouteFromURL(url, []), }; t.test("invalid URL and no route", async () => { @@ -66,7 +66,7 @@ t.test("it returns endpoint based on route", async () => { t.test("it returns endpoint based on relative url", async () => { t.same( matchEndpoints( - { ...context, route: buildRouteFromURL("/posts/3"), url: "/posts/3" }, + { ...context, route: buildRouteFromURL("/posts/3", []), url: "/posts/3" }, [ { method: "POST", diff --git a/library/index.ts b/library/index.ts index ab2b6925c..88243859b 100644 --- a/library/index.ts +++ b/library/index.ts @@ -15,6 +15,7 @@ import { isESM } from "./helpers/isESM"; import { checkIndexImportGuard } from "./helpers/indexImportGuard"; import { setRateLimitGroup } from "./ratelimiting/group"; import { isLibBundled } from "./helpers/isLibBundled"; +import { addRouteParam } from "./agent/addRouteParam"; // Prevent logging twice / trying to start agent twice if (!isNewHookSystemUsed()) { @@ -51,6 +52,7 @@ export { addKoaMiddleware, addRestifyMiddleware, setRateLimitGroup, + addRouteParam, }; // Required for ESM / TypeScript default export support @@ -67,4 +69,5 @@ export default { addKoaMiddleware, addRestifyMiddleware, setRateLimitGroup, + addRouteParam, }; diff --git a/library/sources/FunctionsFramework.ts b/library/sources/FunctionsFramework.ts index 5d8c02c98..7d931e860 100644 --- a/library/sources/FunctionsFramework.ts +++ b/library/sources/FunctionsFramework.ts @@ -1,4 +1,5 @@ /* eslint-disable max-lines-per-function */ +import { getRegisteredRouteParams } from "../agent/addRouteParam"; import { getInstance } from "../agent/AgentSingleton"; import { getContext, runWithContext } from "../agent/Context"; import { Hooks } from "../agent/hooks/Hooks"; @@ -69,7 +70,7 @@ export function createCloudFunctionWrapper(fn: HttpFunction): HttpFunction { cookies: req.cookies ? req.cookies : {}, routeParams: {}, source: "cloud-function/http", - route: buildRouteFromURL(url), + route: buildRouteFromURL(url, getRegisteredRouteParams()), }, async () => { try { diff --git a/library/sources/express/contextFromRequest.ts b/library/sources/express/contextFromRequest.ts index 2a89ccb79..e1765bb76 100644 --- a/library/sources/express/contextFromRequest.ts +++ b/library/sources/express/contextFromRequest.ts @@ -1,4 +1,5 @@ import type { Request } from "express"; +import { getRegisteredRouteParams } from "../../agent/addRouteParam"; import { Context } from "../../agent/Context"; import { buildRouteFromURL } from "../../helpers/buildRouteFromURL"; import { getIPAddressFromRequest } from "../../helpers/getIPAddressFromRequest"; @@ -20,7 +21,7 @@ export function contextFromRequest(req: Request): Context { /* c8 ignore next */ cookies: req.cookies ? req.cookies : {}, source: "express", - route: buildRouteFromURL(url), + route: buildRouteFromURL(url, getRegisteredRouteParams()), subdomains: req.subdomains, }; } diff --git a/library/sources/fastify/contextFromRequest.ts b/library/sources/fastify/contextFromRequest.ts index ea63fbbeb..1a9e0f3f9 100644 --- a/library/sources/fastify/contextFromRequest.ts +++ b/library/sources/fastify/contextFromRequest.ts @@ -1,4 +1,5 @@ import type { FastifyRequest } from "fastify"; +import { getRegisteredRouteParams } from "../../agent/addRouteParam"; import { Context } from "../../agent/Context"; import { buildRouteFromURL } from "../../helpers/buildRouteFromURL"; import { getIPAddressFromRequest } from "../../helpers/getIPAddressFromRequest"; @@ -21,6 +22,6 @@ export function contextFromRequest(req: FastifyRequest): Context { // @ts-expect-error not typed cookies: req.cookies ? req.cookies : {}, source: "fastify", - route: buildRouteFromURL(req.url), + route: buildRouteFromURL(req.url, getRegisteredRouteParams()), }; } diff --git a/library/sources/hapi/contextFromRequest.ts b/library/sources/hapi/contextFromRequest.ts index 4bfabae8c..e8fd19303 100644 --- a/library/sources/hapi/contextFromRequest.ts +++ b/library/sources/hapi/contextFromRequest.ts @@ -1,4 +1,5 @@ import type { Request } from "@hapi/hapi"; +import { getRegisteredRouteParams } from "../../agent/addRouteParam"; import { Context } from "../../agent/Context"; import { buildRouteFromURL } from "../../helpers/buildRouteFromURL"; import { getIPAddressFromRequest } from "../../helpers/getIPAddressFromRequest"; @@ -18,6 +19,6 @@ export function contextFromRequest(req: Request): Context { /* c8 ignore next */ cookies: req.state || {}, source: "hapi", - route: buildRouteFromURL(req.url.toString()), + route: buildRouteFromURL(req.url.toString(), getRegisteredRouteParams()), }; } diff --git a/library/sources/hono/contextFromRequest.ts b/library/sources/hono/contextFromRequest.ts index 27dfa56c4..e40609bd8 100644 --- a/library/sources/hono/contextFromRequest.ts +++ b/library/sources/hono/contextFromRequest.ts @@ -1,4 +1,5 @@ import type { Context as HonoContext } from "hono"; +import { getRegisteredRouteParams } from "../../agent/addRouteParam"; import { Context, getContext } from "../../agent/Context"; import { buildRouteFromURL } from "../../helpers/buildRouteFromURL"; import { getIPAddressFromRequest } from "../../helpers/getIPAddressFromRequest"; @@ -29,6 +30,6 @@ export function contextFromRequest(c: HonoContext): Context { /* c8 ignore next */ cookies: cookieHeader ? parse(cookieHeader) : {}, source: "hono", - route: buildRouteFromURL(req.url), + route: buildRouteFromURL(req.url, getRegisteredRouteParams()), }; } diff --git a/library/sources/http-server/contextFromRequest.ts b/library/sources/http-server/contextFromRequest.ts index 542b98d89..eece79501 100644 --- a/library/sources/http-server/contextFromRequest.ts +++ b/library/sources/http-server/contextFromRequest.ts @@ -1,4 +1,5 @@ import type { IncomingMessage } from "http"; +import { getRegisteredRouteParams } from "../../agent/addRouteParam"; import { Context } from "../../agent/Context"; import { buildRouteFromURL } from "../../helpers/buildRouteFromURL"; import { getIPAddressFromRequest } from "../../helpers/getIPAddressFromRequest"; @@ -31,7 +32,9 @@ export function contextFromRequest( url: req.url, method: req.method, headers: req.headers, - route: req.url ? buildRouteFromURL(req.url) : undefined, + route: req.url + ? buildRouteFromURL(req.url, getRegisteredRouteParams()) + : undefined, query: queryObject, source: `${module}.createServer`, routeParams: {}, diff --git a/library/sources/http-server/http2/contextFromStream.ts b/library/sources/http-server/http2/contextFromStream.ts index 29cc6572b..c4166442f 100644 --- a/library/sources/http-server/http2/contextFromStream.ts +++ b/library/sources/http-server/http2/contextFromStream.ts @@ -1,3 +1,4 @@ +import { getRegisteredRouteParams } from "../../../agent/addRouteParam"; import { Context } from "../../../agent/Context"; import { buildRouteFromURL } from "../../../helpers/buildRouteFromURL"; import { getIPAddressFromRequest } from "../../../helpers/getIPAddressFromRequest"; @@ -27,7 +28,7 @@ export function contextFromStream( url: url, method: headers[":method"] as string, headers: headers, - route: url ? buildRouteFromURL(url) : undefined, + route: url ? buildRouteFromURL(url, getRegisteredRouteParams()) : undefined, query: queryObject, source: `${module}.createServer`, routeParams: {}, diff --git a/library/sources/koa/contextFromRequest.ts b/library/sources/koa/contextFromRequest.ts index de7c9f3df..ee0968cf0 100644 --- a/library/sources/koa/contextFromRequest.ts +++ b/library/sources/koa/contextFromRequest.ts @@ -1,4 +1,5 @@ import type { Context as KoaContext } from "koa"; +import { getRegisteredRouteParams } from "../../agent/addRouteParam"; import { Context } from "../../agent/Context"; import { buildRouteFromURL } from "../../helpers/buildRouteFromURL"; import { getIPAddressFromRequest } from "../../helpers/getIPAddressFromRequest"; @@ -20,7 +21,7 @@ export function contextFromRequest(ctx: KoaContext): Context { query: ctx.request.query, cookies: ctx.req.headers.cookie ? parseCookies(ctx.req.headers.cookie) : {}, source: "koa", - route: buildRouteFromURL(ctx.request.href), + route: buildRouteFromURL(ctx.request.href, getRegisteredRouteParams()), subdomains: ctx.request.subdomains, }; } diff --git a/library/sources/restify/contextFromRequest.ts b/library/sources/restify/contextFromRequest.ts index 7fa91152f..d72898e28 100644 --- a/library/sources/restify/contextFromRequest.ts +++ b/library/sources/restify/contextFromRequest.ts @@ -1,4 +1,5 @@ import { IncomingMessage } from "http"; +import { getRegisteredRouteParams } from "../../agent/addRouteParam"; import { Context } from "../../agent/Context"; import { buildRouteFromURL } from "../../helpers/buildRouteFromURL"; import { getIPAddressFromRequest } from "../../helpers/getIPAddressFromRequest"; @@ -27,6 +28,6 @@ export function contextFromRequest(req: RestifyRequest): Context { query: isPlainObject(req.query) ? req.query : {}, cookies: req.headers?.cookie ? parse(req.headers.cookie) : {}, source: "restify", - route: buildRouteFromURL(req.href()), + route: buildRouteFromURL(req.href(), getRegisteredRouteParams()), }; } From 8545c6d4e9af2be7abb48596ccb0907cd48da428 Mon Sep 17 00:00:00 2001 From: Hans Ott Date: Wed, 26 Nov 2025 11:09:07 +0100 Subject: [PATCH 4/4] Fix test --- library/helpers/buildRouteFromURL.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/helpers/buildRouteFromURL.test.ts b/library/helpers/buildRouteFromURL.test.ts index 8066f5d89..b74b45294 100644 --- a/library/helpers/buildRouteFromURL.test.ts +++ b/library/helpers/buildRouteFromURL.test.ts @@ -164,7 +164,7 @@ t.test("it replaces secrets", async () => { t.test("it replaces BSON ObjectIDs", async () => { t.same( // @ts-expect-error It says that the expression isn't callable - buildRouteFromURL(`/posts/${ObjectID().toHexString()}`), + buildRouteFromURL(`/posts/${ObjectID().toHexString()}`, []), "/posts/:objectId" ); t.same(