diff --git a/packages/react-native/Libraries/Blob/URL.js b/packages/react-native/Libraries/Blob/URL.js index c0f496e1954f0d..f4f879a51214b4 100644 --- a/packages/react-native/Libraries/Blob/URL.js +++ b/packages/react-native/Libraries/Blob/URL.js @@ -90,11 +90,15 @@ export class URL { } } + // Only add trailing slash if URL has no path (just domain) if ( !this._url.endsWith('/') && !(this._url.includes('?') || this._url.includes('#')) ) { - this._url += '/'; + const afterProtocol = this._url.split('://')[1]; + if (afterProtocol && !afterProtocol.includes('/')) { + this._url += '/'; + } } } else { if (typeof base === 'string') { @@ -170,6 +174,24 @@ export class URL { return searchMatch ? `?${searchMatch[1]}` : ''; } + set search(value: string) { + // Remove leading '?' if present + const searchString = value.startsWith('?') ? value.slice(1) : value; + + // Update the internal URL + const baseUrl = this._url.split('?')[0].split('#')[0]; + const hash = this.hash; + + if (searchString) { + this._url = baseUrl + '?' + searchString + hash; + } else { + this._url = baseUrl + hash; + } + + // Reset the searchParams instance so it gets recreated with new values + this._searchParamsInstance = null; + } + get searchParams(): URLSearchParams { if (this._searchParamsInstance == null) { this._searchParamsInstance = new URLSearchParams(this.search); @@ -185,10 +207,19 @@ export class URL { if (this._searchParamsInstance === null) { return this._url; } + + // Remove existing search params and hash from the URL + const baseUrl = this._url.split('?')[0].split('#')[0]; + const hash = this.hash; + // $FlowFixMe[incompatible-use] const instanceString = this._searchParamsInstance.toString(); - const separator = this._url.indexOf('?') > -1 ? '&' : '?'; - return this._url + separator + instanceString; + + if (instanceString) { + return baseUrl + '?' + instanceString + hash; + } else { + return baseUrl + hash; + } } get username(): string { diff --git a/packages/react-native/Libraries/Blob/__tests__/URL-test.js b/packages/react-native/Libraries/Blob/__tests__/URL-test.js index fc216b05618bdc..72467dc08ebbc7 100644 --- a/packages/react-native/Libraries/Blob/__tests__/URL-test.js +++ b/packages/react-native/Libraries/Blob/__tests__/URL-test.js @@ -139,5 +139,33 @@ describe('URL', function () { ); unsortedParams.sort(); expect(unsortedParams.toString()).toBe('a=first&b=second&c=third&z=last'); + + // searchParams.set() should replace values not duplicate them + const urlWithSearchParams = new URL( + 'https://example.com/api/data?foo=bar&baz=qux', + ); + urlWithSearchParams.searchParams.set('foo', 'newFoo'); + urlWithSearchParams.searchParams.set('test', '12345'); + expect(urlWithSearchParams.toString()).toBe( + 'https://example.com/api/data?foo=newFoo&baz=qux&test=12345', + ); + + //url.search setter should replace search params + const urlSearchSetter = new URL( + 'https://example.com/api/data?foo=bar&baz=qux', + ); + + urlSearchSetter.search = 'foo=overwritten&hello=world'; + + expect(urlSearchSetter.toString()).toBe( + 'https://example.com/api/data?foo=overwritten&hello=world', + ); + + //URL constructor should not add trailing slash to paths + const urlWithoutTrailing = new URL('https://example.com/path/to/resource'); + + expect(urlWithoutTrailing.toString()).toBe( + 'https://example.com/path/to/resource', + ); }); });