From 93eea0b6d975d68369f737fd153e948080c9eea0 Mon Sep 17 00:00:00 2001 From: "Nolann B." <100787331+nolannbiron@users.noreply.github.com> Date: Fri, 20 Feb 2026 14:40:36 +0100 Subject: [PATCH] Strip trailing slash from OpenAPI server URLs (#4029) --- .changeset/clean-servers-slash.md | 5 +++++ .../src/OpenAPIPathMultipleServers.tsx | 15 +++++++++------ .../react-openapi/src/util/server.test.ts | 19 +++++++++++++++++++ packages/react-openapi/src/util/server.ts | 5 ++++- 4 files changed, 37 insertions(+), 7 deletions(-) create mode 100644 .changeset/clean-servers-slash.md diff --git a/.changeset/clean-servers-slash.md b/.changeset/clean-servers-slash.md new file mode 100644 index 000000000..78677d378 --- /dev/null +++ b/.changeset/clean-servers-slash.md @@ -0,0 +1,5 @@ +--- +"@gitbook/react-openapi": patch +--- + +Strip trailing slash from OpenAPI server URLs to avoid double slashes in rendered paths diff --git a/packages/react-openapi/src/OpenAPIPathMultipleServers.tsx b/packages/react-openapi/src/OpenAPIPathMultipleServers.tsx index c5f985666..d3b49ba2f 100644 --- a/packages/react-openapi/src/OpenAPIPathMultipleServers.tsx +++ b/packages/react-openapi/src/OpenAPIPathMultipleServers.tsx @@ -7,7 +7,7 @@ import { OpenAPITooltip } from './OpenAPITooltip'; import type { OpenAPIClientContext } from './context'; import { formatPath } from './formatPath'; import type { OpenAPIServerWithCustomProperties } from './types'; -import { getDefaultServerURL } from './util/server'; +import { getDefaultServerURL, interpolateServerURL } from './util/server'; import { createStateKey } from './utils'; export const serversStateKey = createStateKey('servers'); @@ -29,11 +29,14 @@ export function OpenAPIPathMultipleServers( .filter( (server): server is OpenAPIServerWithCustomProperties & { url: string } => !!server.url ) - .map((server) => ({ - key: server.url, - label: server.url, - description: server.description, - })); + .map((server) => { + const url = interpolateServerURL(server); + return { + key: url, + label: url, + description: server.description, + }; + }); return ( { expect(result).toBe('https://{username}.example.com/{basePath}'); }); + it('strips trailing slash from the server URL', () => { + const server: OpenAPIV3.ServerObject = { + url: '/butler/api/', + }; + const result = interpolateServerURL(server); + expect(result).toBe('/butler/api'); + }); + + it('strips trailing slash after variable interpolation', () => { + const server: OpenAPIV3.ServerObject = { + url: 'https://example.com/{basePath}/', + variables: { + basePath: { default: 'v1' }, + }, + }; + const result = interpolateServerURL(server); + expect(result).toBe('https://example.com/v1'); + }); + it('returns the URL with mixed placeholders and default values', () => { const server: OpenAPIV3.ServerObject = { url: 'https://{username}.example.com/{basePath}', diff --git a/packages/react-openapi/src/util/server.ts b/packages/react-openapi/src/util/server.ts index 81379598f..0ebe175d5 100644 --- a/packages/react-openapi/src/util/server.ts +++ b/packages/react-openapi/src/util/server.ts @@ -19,7 +19,7 @@ export function getDefaultServerURL(servers: OpenAPIV3.ServerObject[]): string { export function interpolateServerURL(server: OpenAPIV3.ServerObject) { const parts = parseServerURL(server?.url ?? ''); - return parts + const url = parts .map((part) => { if (part.kind === 'text') { return part.text; @@ -27,6 +27,9 @@ export function interpolateServerURL(server: OpenAPIV3.ServerObject) { return server.variables?.[part.name]?.default ?? `{${part.name}}`; }) .join(''); + + // Remove trailing slash to avoid double slashes when concatenated with paths + return url.endsWith('/') ? url.slice(0, -1) : url; } function parseServerURL(url: string) {