From 2ee4cdba0b899af003ca875ea7b16932f8170f24 Mon Sep 17 00:00:00 2001 From: 5ZYSZ3K Date: Fri, 24 Jul 2026 18:30:31 +0200 Subject: [PATCH] fix: stop adding trailing / to absolute urls --- .../__tests__/normalizeResourceLocator.test.ts | 15 +++++++++++++++ .../src/helpers/normalizeResourceLocator.ts | 15 +++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/packages/render/src/helpers/__tests__/normalizeResourceLocator.test.ts b/packages/render/src/helpers/__tests__/normalizeResourceLocator.test.ts index bd98525a..28688008 100644 --- a/packages/render/src/helpers/__tests__/normalizeResourceLocator.test.ts +++ b/packages/render/src/helpers/__tests__/normalizeResourceLocator.test.ts @@ -42,6 +42,21 @@ describe('normalizeResourceLocator', () => { normalizeResourceLocator('//bar.com/baz', 'https://foo.com/') ).toEqual('https://bar.com/baz'); }); + it('should not append a trailing slash to an absolute URL with a path (issue #47)', () => { + expect( + normalizeResourceLocator('https://example.com/files/report.pdf') + ).toEqual('https://example.com/files/report.pdf'); + }); + it('should return an absolute URL untouched even when a base URL is provided (issue #47)', () => { + expect( + normalizeResourceLocator('https://bit.ly/abc123', 'https://foo.com/') + ).toEqual('https://bit.ly/abc123'); + }); + it('should preserve query and fragment on an absolute URL (issue #47)', () => { + expect( + normalizeResourceLocator('https://example.com/a/b?q=1#frag') + ).toEqual('https://example.com/a/b?q=1#frag'); + }); it('should pass regression #', () => { expect( normalizeResourceLocator( diff --git a/packages/render/src/helpers/normalizeResourceLocator.ts b/packages/render/src/helpers/normalizeResourceLocator.ts index 5106dfcb..95f095f6 100644 --- a/packages/render/src/helpers/normalizeResourceLocator.ts +++ b/packages/render/src/helpers/normalizeResourceLocator.ts @@ -1,3 +1,10 @@ +/** + * Matches a URL that already carries a scheme, e.g. `https://`, `ftp://`. + * Protocol-relative URLs (`//host/path`) are intentionally excluded so they + * still get resolved against the base URL. + */ +const ABSOLUTE_URL_REGEX = /^[a-z][a-z0-9+.-]*:\/\//i; + /** * This function normalize relative and protocol-relative URLs to absolute * URLs as per {@link https://tools.ietf.org/html/rfc1808 | RFC1808}. @@ -10,6 +17,14 @@ export default function normalizeResourceLocator( baseUrl?: string ) { try { + // If the URL is already absolute, return it untouched. We deliberately + // avoid round-tripping through the URL constructor here because React + // Native's URL polyfill appends a trailing slash to a path-bearing URL + // (e.g. `https://example.com/file.pdf` -> `https://example.com/file.pdf/`), + // corrupting otherwise-valid links. See issue #47. + if (ABSOLUTE_URL_REGEX.test(url)) { + return url; + } if (!baseUrl) { // Try to parse as absolute URL return new URL(url).href;