diff --git a/src/platform/packages/shared/kbn-apm-synthtrace/src/lib/shared/base_kibana_client.ts b/src/platform/packages/shared/kbn-apm-synthtrace/src/lib/shared/base_kibana_client.ts index e4d11b07bf76b..5b6fe0ea8eb92 100644 --- a/src/platform/packages/shared/kbn-apm-synthtrace/src/lib/shared/base_kibana_client.ts +++ b/src/platform/packages/shared/kbn-apm-synthtrace/src/lib/shared/base_kibana_client.ts @@ -11,9 +11,9 @@ import fetch from 'node-fetch'; import { RequestInit } from 'node-fetch'; -import Path from 'path'; import { kibanaHeaders } from './client_headers'; import { getFetchAgent } from '../../cli/utils/ssl'; +import { normalizeUrl } from '../utils/normalize_url'; type KibanaClientFetchOptions = RequestInit; @@ -33,18 +33,20 @@ export class KibanaClient { } fetch(pathname: string, options: KibanaClientFetchOptions): Promise { - const url = Path.join(this.target, pathname); - return fetch(url, { + const pathnameWithLeadingSlash = pathname.startsWith('/') ? pathname : `/${pathname}`; + const url = new URL(`${this.target}${pathnameWithLeadingSlash}`); + const normalizedUrl = normalizeUrl(url.toString()); + return fetch(normalizedUrl, { ...options, headers: { ...this.headers, ...options.headers, }, - agent: getFetchAgent(url), + agent: getFetchAgent(normalizedUrl), }).then(async (response) => { if (response.status >= 400) { throw new KibanaClientHttpError( - `Response error for ${options.method?.toUpperCase() ?? 'GET'} ${url}`, + `Response error for ${options.method?.toUpperCase() ?? 'GET'} ${normalizedUrl}`, response.status, await response.json().catch((error) => { return undefined; diff --git a/src/platform/packages/shared/kbn-apm-synthtrace/src/lib/utils/normalize_url.ts b/src/platform/packages/shared/kbn-apm-synthtrace/src/lib/utils/normalize_url.ts new file mode 100644 index 0000000000000..9acc8cef7d5d0 --- /dev/null +++ b/src/platform/packages/shared/kbn-apm-synthtrace/src/lib/utils/normalize_url.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +export function normalizeUrl(url: string): string { + return url.replace(/([^:]\/)\/+/g, '$1'); +} diff --git a/src/platform/packages/shared/kbn-apm-synthtrace/src/lib/utils/normalized_url.test.ts b/src/platform/packages/shared/kbn-apm-synthtrace/src/lib/utils/normalized_url.test.ts new file mode 100644 index 0000000000000..6434d66f57d44 --- /dev/null +++ b/src/platform/packages/shared/kbn-apm-synthtrace/src/lib/utils/normalized_url.test.ts @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { normalizeUrl } from './normalize_url'; + +describe('normalizeUrl', () => { + it('should remove unnecessary double slashes from the URL', () => { + const url = 'http://example.com//some//path///to/resource'; + const result = normalizeUrl(url); + expect(result).toBe('http://example.com/some/path/to/resource'); + }); + + it('should preserve the protocol slashes', () => { + const url = 'http:///example.com'; + const result = normalizeUrl(url); + expect(result).toBe('http://example.com'); + }); + + it('should handle URLs with query parameters', () => { + const url = 'http://example.com//some/path?key=value&key2=value2'; + const result = normalizeUrl(url); + expect(result).toBe('http://example.com/some/path?key=value&key2=value2'); + }); + + it('should handle URLs with port', () => { + const url = 'http://example:3000//some/path?key=value&key2=value2'; + const result = normalizeUrl(url); + expect(result).toBe('http://example:3000/some/path?key=value&key2=value2'); + }); + + it('should handle URLs with localhost port and extra path', () => { + const url = 'http://localhost:3000//some/path//more/path'; + const result = normalizeUrl(url); + expect(result).toBe('http://localhost:3000/some/path/more/path'); + }); + + it('should handle URLs with trailing slashes', () => { + const url = 'http://example.com/some/path///'; + const result = normalizeUrl(url); + expect(result).toBe('http://example.com/some/path/'); + }); + + it('should handle URLs without double slashes', () => { + const url = 'http://example.com/some/path'; + const result = normalizeUrl(url); + expect(result).toBe('http://example.com/some/path'); + }); + + it('should handle URLs with no path', () => { + const url = 'http://example.com'; + const result = normalizeUrl(url); + expect(result).toBe('http://example.com'); + }); + + it('should handle empty strings', () => { + const url = ''; + const result = normalizeUrl(url); + expect(result).toBe(''); + }); + + it('should handle URLs with differnt protocol', () => { + const url = 'ftp://example.com//some/path'; + const result = normalizeUrl(url); + expect(result).toBe('ftp://example.com/some/path'); + }); +});