From 0defaeb00c4b7e4a1833ef4dd6f8d96ff60d355f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Mar 2026 18:28:15 +0000 Subject: [PATCH 1/4] Initial plan From 06c55d143b5962a9a6e58e00decb6398deb60eed Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Mar 2026 18:33:43 +0000 Subject: [PATCH 2/4] fix: preserve trailing slash in route joining when it would not result in double slash Co-authored-by: timotheeguerin <1031227+timotheeguerin@users.noreply.github.com> --- packages/http/src/route.ts | 15 +++++++++------ packages/http/test/routes.test.ts | 19 +++++++++++++++++++ 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/packages/http/src/route.ts b/packages/http/src/route.ts index 6a49a332498..2360d77312c 100644 --- a/packages/http/src/route.ts +++ b/packages/http/src/route.ts @@ -36,22 +36,25 @@ function needsSlashPrefix(fragment: string) { ); } -function normalizeFragment(fragment: string, trimLast = false) { +function normalizeFragment(fragment: string) { if (needsSlashPrefix(fragment)) { // Insert the default separator fragment = `/${fragment}`; } - if (trimLast && fragment[fragment.length - 1] === "/") { - return fragment.slice(0, -1); - } return fragment; } export function joinPathSegments(rest: string[]) { let current = ""; - for (const [index, segment] of rest.entries()) { - current += normalizeFragment(segment, index < rest.length - 1); + for (const segment of rest) { + const normalized = normalizeFragment(segment); + // Merge trailing and leading slashes to avoid double slashes + if (current.endsWith("/") && normalized.startsWith("/")) { + current += normalized.slice(1); + } else { + current += normalized; + } } return current; } diff --git a/packages/http/test/routes.test.ts b/packages/http/test/routes.test.ts index bdb1e60ac5f..39cec8801f5 100644 --- a/packages/http/test/routes.test.ts +++ b/packages/http/test/routes.test.ts @@ -346,6 +346,25 @@ describe("http: routes", () => { deepStrictEqual(routes, [{ verb: "get", path: `/abc${sep}restype=container`, params: [] }]); }); + + it("keeps trailing / on parent route when joining with child route", async () => { + const routes = await getRoutesFor( + ` + @route("{blobName}/") + interface Container { + @put @route("${sep}some-query=true") op foo(blobName: string): void; + } + `, + ); + + deepStrictEqual(routes, [ + { + verb: "put", + path: `/{blobName}/${sep}some-query=true`, + params: ["blobName"], + }, + ]); + }); }); describe("joinPathSegments", () => { From 96d0848fb22bcd981e64a4d6a0c2d70d97a32fda Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Mar 2026 18:34:13 +0000 Subject: [PATCH 3/4] chore: add changelog entry for trailing slash route fix Co-authored-by: timotheeguerin <1031227+timotheeguerin@users.noreply.github.com> --- ...opilot-fix-regression-http-1-10-1-2026-2-18-18-33-49.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .chronus/changes/copilot-fix-regression-http-1-10-1-2026-2-18-18-33-49.md diff --git a/.chronus/changes/copilot-fix-regression-http-1-10-1-2026-2-18-18-33-49.md b/.chronus/changes/copilot-fix-regression-http-1-10-1-2026-2-18-18-33-49.md new file mode 100644 index 00000000000..0d220f1033f --- /dev/null +++ b/.chronus/changes/copilot-fix-regression-http-1-10-1-2026-2-18-18-33-49.md @@ -0,0 +1,7 @@ +--- +changeKind: fix +packages: + - "@typespec/http" +--- + +Fix route joining to preserve trailing `/` when it would not result in `//` \ No newline at end of file From b0843b4c23f0bd4949cb6a0fa5cb4bd51d92ff8b Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 6 Nov 2026 08:42:55 -0500 Subject: [PATCH 4/4] fix(http): preserve path expansions when joining trailing slashes --- packages/http/src/route.ts | 10 ++++------ packages/http/test/routes.test.ts | 8 ++++++++ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/packages/http/src/route.ts b/packages/http/src/route.ts index dac72dab33a..1e56f249c15 100644 --- a/packages/http/src/route.ts +++ b/packages/http/src/route.ts @@ -48,19 +48,17 @@ function normalizeFragment(fragment: string) { /** * Joins route fragments, adding a slash when a fragment lacks a recognized separator - * and trimming one trailing slash from non-final fragments. Preserves URI-template path expansions. + * and merging overlapping slashes at fragment boundaries. Preserves URI-template path expansions. * An empty input returns an empty string. */ export function joinPathSegments(rest: string[]) { let current = ""; for (const segment of rest) { const normalized = normalizeFragment(segment); - // Merge trailing and leading slashes to avoid double slashes - if (current.endsWith("/") && normalized.startsWith("/")) { - current += normalized.slice(1); - } else { - current += normalized; + if (current.endsWith("/") && (normalized.startsWith("/") || normalized.startsWith("{/"))) { + current = current.slice(0, -1); } + current += normalized; } return current; } diff --git a/packages/http/test/routes.test.ts b/packages/http/test/routes.test.ts index 4ac5df81104..d50b197c5b1 100644 --- a/packages/http/test/routes.test.ts +++ b/packages/http/test/routes.test.ts @@ -640,6 +640,14 @@ describe("uri template", () => { const op = await getOp(`@route("/foo") op foo(${param}): void;`); expect(op.uriTemplate).toEqual(expectedUri); }); + + it.each([`@path(#{style: "path"}) one: string`, "@path one?: string"])( + "merges trailing / with path expansion for %s", + async (param) => { + const op = await getOp(`@route("/foo/") op foo(${param}): void;`); + expect(op.uriTemplate).toEqual("/foo{/one}"); + }, + ); }); describe("warn when path interpolation would result in duplicate //", () => {