From d2ff8ecc68bd6a48fe76bc22ab8404b6cfa42a85 Mon Sep 17 00:00:00 2001 From: Vishal Narkhede Date: Tue, 3 Oct 2023 14:11:40 +0200 Subject: [PATCH] fix: axios param serializer to comply with RFC 3986 (#1180) ## Issue **Fixes**: https://github.com/GetStream/stream-chat-react-native/issues/2235 On iOS 17, all the `get` requests which involve object or array in url params (e.g. `queryMembers`) are failing. In iOS 17, NSURLs are now encoded according to RFC 3986 standards (as specified in https://www.ietf.org/rfc/rfc3986.txt), whereas they used to adhere to RFC 1738/1808 standards in earlier versions. reference: https://developer.apple.com/documentation/foundation/nsurl/1572047-urlwithstring > For apps linked on or after iOS 17 and aligned OS versions, [NSURL](https://developer.apple.com/documentation/foundation/nsurl) parsing has updated from the obsolete RFC 1738/1808 parsing to the same [RFC 3986](https://www.ietf.org/rfc/rfc3986.txt) parsing as [NSURLComponents](https://developer.apple.com/documentation/foundation/nsurlcomponents). This unifies the parsing behaviors of the NSURL and NSURLComponents APIs. Now, NSURL automatically percent- and IDNA-encodes invalid characters to help create a valid URL. And axios on the other hand doesn't adhere to RFC 3986 - it doesn't encode brackets such as `[`, `{` etc ([source](https://github.com/axios/axios/blob/v1.x/lib/helpers/buildURL.js#L20)). As a result of this, whenever `NSUrl` encounters a reserved character, such as `[`, the parser will percent encode all possible characters in the URL, including %. And this results into double encoded url, which doesn't pass the validation on Stream backend. E.g., ``` payload=%257B%2522type%2522:%2522messaging%2522,%2522id%2522:%2522campaign-test-channel-0%2522,%2522sort%2522:%5B%5D,%2522filter_conditions%2522:%257B%2522name%2522:%2522Robert%2522%257D%257D ``` And this is a known issue with axios - https://github.com/axios/axios/issues/4432 React Native tried handling this issue here - but later they reverted the fix for some other reason: - https://github.com/facebook/react-native/commit/9841bd81852d59608fe3566b17831d6d42eb7dcf - reverted https://github.com/facebook/react-native/commit/2be409ff55b50563a5f123f7823fa7cdb72dbef9 ## Solution So we need to override default param serialization of axios, and make sure that the url param string is RFC 3986 compliant - if param is object or array, simply stringify it and then encode it. - for the rest, do a normal uri encoding --- src/client.ts | 3 +++ src/utils.ts | 14 ++++++++++++++ test/unit/utils.js | 31 ++++++++++++++++++++++++++++++- 3 files changed, 47 insertions(+), 1 deletion(-) diff --git a/src/client.ts b/src/client.ts index ba8e20370..a5226a20f 100644 --- a/src/client.ts +++ b/src/client.ts @@ -14,6 +14,7 @@ import { WSConnectionFallback } from './connection_fallback'; import { isErrorResponse, isWSFailure } from './errors'; import { addFileToFormData, + axiosParamsSerializer, chatCodes, isFunction, isOnline, @@ -305,6 +306,8 @@ export class StreamChat void) { window.removeEventListener('online', cb); } } + +export const axiosParamsSerializer: AxiosRequestConfig['paramsSerializer'] = (params) => { + const newParams = []; + for (const k in params) { + if (Array.isArray(params[k]) || typeof params[k] === 'object') { + newParams.push(`${k}=${encodeURIComponent(JSON.stringify(params[k]))}`); + } else { + newParams.push(`${k}=${encodeURIComponent(params[k])}`); + } + } + + return newParams.join('&'); +}; diff --git a/test/unit/utils.js b/test/unit/utils.js index 71136822b..32fe3719f 100644 --- a/test/unit/utils.js +++ b/test/unit/utils.js @@ -1,5 +1,5 @@ import chai from 'chai'; -import { generateUUIDv4, normalizeQuerySort } from '../../src/utils'; +import { axiosParamsSerializer, generateUUIDv4, normalizeQuerySort } from '../../src/utils'; import sinon from 'sinon'; const expect = chai.expect; @@ -69,3 +69,32 @@ describe('test if sort is deterministic', () => { expect(sort[3].direction).to.be.equal(-1); }); }); + +describe.only('axiosParamsSerializer', () => { + const testCases = [ + { + input: { + a: 1, + b: 2, + c: null, + }, + output: 'a=1&b=2&c=null', + }, + { + input: { + a: { + b: 1, + c: 2, + d: null, + }, + b: [1, 2, 3], + }, + output: 'a=%7B%22b%22%3A1%2C%22c%22%3A2%2C%22d%22%3Anull%7D&b=%5B1%2C2%2C3%5D', + }, + ]; + it('should serialize params', () => { + for (const { input, output } of testCases) { + expect(axiosParamsSerializer(input)).to.equal(output); + } + }); +});