Skip to content

Commit d2ff8ec

Browse files
fix: axios param serializer to comply with RFC 3986 (#1180)
## Issue **Fixes**: GetStream/stream-chat-react-native#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 - axios/axios#4432 React Native tried handling this issue here - but later they reverted the fix for some other reason: - facebook/react-native@9841bd8 - reverted facebook/react-native@2be409f ## 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
1 parent 88ef7ef commit d2ff8ec

File tree

3 files changed

+47
-1
lines changed

3 files changed

+47
-1
lines changed

src/client.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { WSConnectionFallback } from './connection_fallback';
1414
import { isErrorResponse, isWSFailure } from './errors';
1515
import {
1616
addFileToFormData,
17+
axiosParamsSerializer,
1718
chatCodes,
1819
isFunction,
1920
isOnline,
@@ -305,6 +306,8 @@ export class StreamChat<StreamChatGenerics extends ExtendableGenerics = DefaultG
305306
this.defaultWSTimeoutWithFallback = 6000;
306307
this.defaultWSTimeout = 15000;
307308

309+
this.axiosInstance.defaults.paramsSerializer = axiosParamsSerializer;
310+
308311
/**
309312
* logger function should accept 3 parameters:
310313
* @param logLevel string

src/utils.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import FormData from 'form-data';
22
import { AscDesc, ExtendableGenerics, DefaultGenerics, OwnUserBase, OwnUserResponse, UserResponse } from './types';
3+
import { AxiosRequestConfig } from 'axios';
34

45
/**
56
* logChatPromiseExecution - utility function for logging the execution of a promise..
@@ -245,3 +246,16 @@ export function removeConnectionEventListeners(cb: (e: Event) => void) {
245246
window.removeEventListener('online', cb);
246247
}
247248
}
249+
250+
export const axiosParamsSerializer: AxiosRequestConfig['paramsSerializer'] = (params) => {
251+
const newParams = [];
252+
for (const k in params) {
253+
if (Array.isArray(params[k]) || typeof params[k] === 'object') {
254+
newParams.push(`${k}=${encodeURIComponent(JSON.stringify(params[k]))}`);
255+
} else {
256+
newParams.push(`${k}=${encodeURIComponent(params[k])}`);
257+
}
258+
}
259+
260+
return newParams.join('&');
261+
};

test/unit/utils.js

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import chai from 'chai';
2-
import { generateUUIDv4, normalizeQuerySort } from '../../src/utils';
2+
import { axiosParamsSerializer, generateUUIDv4, normalizeQuerySort } from '../../src/utils';
33
import sinon from 'sinon';
44

55
const expect = chai.expect;
@@ -69,3 +69,32 @@ describe('test if sort is deterministic', () => {
6969
expect(sort[3].direction).to.be.equal(-1);
7070
});
7171
});
72+
73+
describe.only('axiosParamsSerializer', () => {
74+
const testCases = [
75+
{
76+
input: {
77+
a: 1,
78+
b: 2,
79+
c: null,
80+
},
81+
output: 'a=1&b=2&c=null',
82+
},
83+
{
84+
input: {
85+
a: {
86+
b: 1,
87+
c: 2,
88+
d: null,
89+
},
90+
b: [1, 2, 3],
91+
},
92+
output: 'a=%7B%22b%22%3A1%2C%22c%22%3A2%2C%22d%22%3Anull%7D&b=%5B1%2C2%2C3%5D',
93+
},
94+
];
95+
it('should serialize params', () => {
96+
for (const { input, output } of testCases) {
97+
expect(axiosParamsSerializer(input)).to.equal(output);
98+
}
99+
});
100+
});

0 commit comments

Comments
 (0)