diff --git a/src/matcher.ts b/src/matcher.ts index 4cd6b9d..05e617f 100644 --- a/src/matcher.ts +++ b/src/matcher.ts @@ -12,6 +12,14 @@ interface CompiledPath { template: string; } +function decodePathParameter(value: string): string { + try { + return decodeURIComponent(value); + } catch { + return value; + } +} + /** * Compile OpenAPI path templates into regexes for efficient matching. * E.g. "/users/{id}" → /^\/users\/([^/]+)$/ @@ -79,7 +87,7 @@ export function createMatcher(spec: OpenApiSpec) { const pathParams: Record = {}; for (let i = 0; i < entry.paramNames.length; i += 1) { - pathParams[entry.paramNames[i]] = match[i + 1]; + pathParams[entry.paramNames[i]] = decodePathParameter(match[i + 1]); } return { diff --git a/tests/matcher.test.ts b/tests/matcher.test.ts index 3617276..dedf111 100644 --- a/tests/matcher.test.ts +++ b/tests/matcher.test.ts @@ -80,6 +80,18 @@ describe("matchOperation", () => { assert.equal(result.pathParams.memberId, "m2"); }); + it("decodes percent-encoded path parameters", () => { + const result = match("GET", "/users/hello%20world%2Bplus"); + assert.ok(result); + assert.equal(result.pathParams.id, "hello world+plus"); + }); + + it("preserves malformed percent-encoded path parameters", () => { + const result = match("GET", "/users/bad%2"); + assert.ok(result); + assert.equal(result.pathParams.id, "bad%2"); + }); + it("returns null for unmatched path", () => { const result = match("GET", "/unknown"); assert.equal(result, null);