fix(backend-registry): preserve URL fragments in withBackendSelectionParams (#16619)

Co-authored-by: VascoSch92 <vasco.schiavo@protonmail.com>
This commit is contained in:
chrislazar25
2026-08-19 09:07:29 +00:00
committed by GitHub
co-authored by VascoSch92
parent d636ae6893
commit 551e9a9ee6
2 changed files with 83 additions and 3 deletions
@@ -65,6 +65,73 @@ describe("withBackendSelectionParams", () => {
`/conversations/abc?tab=files&${BACKEND_QUERY_PARAM}=local-1`,
);
});
it("keeps a fragment after existing query parameters intact and after the query", () => {
const path = withBackendSelectionParams(
"/conversations/abc?tab=files#detail",
{
backend: localBackend,
orgId: null,
},
);
expect(path).toBe(
`/conversations/abc?tab=files&${BACKEND_QUERY_PARAM}=local-1#detail`,
);
});
it("keeps a fragment on a path without query parameters after the query", () => {
const path = withBackendSelectionParams("/conversations/abc#detail", {
backend: localBackend,
orgId: null,
});
expect(path).toBe(
`/conversations/abc?${BACKEND_QUERY_PARAM}=local-1#detail`,
);
});
it("does not treat a ? inside the fragment as a query separator", () => {
const path = withBackendSelectionParams("/conversations/abc#detail?x=1", {
backend: localBackend,
orgId: null,
});
expect(path).toBe(
`/conversations/abc?${BACKEND_QUERY_PARAM}=local-1#detail?x=1`,
);
});
it("round-trips an empty fragment verbatim", () => {
const path = withBackendSelectionParams("/conversations/abc#", {
backend: localBackend,
orgId: null,
});
expect(path).toBe(`/conversations/abc?${BACKEND_QUERY_PARAM}=local-1#`);
});
it("keeps the org id and the fragment together", () => {
const path = withBackendSelectionParams("/conversations/abc#detail", {
backend: cloudBackend,
orgId: "org-7",
});
expect(path).toBe(
`/conversations/abc?${BACKEND_QUERY_PARAM}=prod&${ORG_QUERY_PARAM}=org-7#detail`,
);
});
it("keeps query data that itself contains a ?", () => {
const path = withBackendSelectionParams("/conversations/abc?next=/a?b=1", {
backend: localBackend,
orgId: null,
});
expect(path).toBe(
`/conversations/abc?next=%2Fa%3Fb%3D1&${BACKEND_QUERY_PARAM}=local-1`,
);
});
});
describe("readBackendSelectionFromUrl", () => {
+16 -3
View File
@@ -19,7 +19,8 @@ export const ORG_QUERY_PARAM = "org";
/**
* Append the active backend identity to an in-app path so opening it in a new
* browsing context resolves against the same backend.
* browsing context resolves against the same backend. Any fragment on the
* path is preserved verbatim and kept after the query string.
*/
export function withBackendSelectionParams(
path: string,
@@ -28,12 +29,24 @@ export function withBackendSelectionParams(
const { backend, orgId } = active;
if (!backend.id) return path;
const [pathname, existingSearch = ""] = path.split("?");
const hashIndex = path.indexOf("#");
const fragment = hashIndex === -1 ? "" : path.slice(hashIndex);
const withoutFragment = hashIndex === -1 ? path : path.slice(0, hashIndex);
// Split at the *first* `?` only. A later `?` is ordinary query data (a
// `next=` redirect carrying its own query, say), and `split("?")` would
// silently drop everything past it.
const queryIndex = withoutFragment.indexOf("?");
const pathname =
queryIndex === -1 ? withoutFragment : withoutFragment.slice(0, queryIndex);
const existingSearch =
queryIndex === -1 ? "" : withoutFragment.slice(queryIndex + 1);
const params = new URLSearchParams(existingSearch);
params.set(BACKEND_QUERY_PARAM, backend.id);
if (orgId) params.set(ORG_QUERY_PARAM, orgId);
return `${pathname}?${params.toString()}`;
return `${pathname}?${params.toString()}${fragment}`;
}
/**