Skip to content

Commit

Permalink
fix: axios param serializer to comply with RFC 3986 (#1180)
Browse files Browse the repository at this point in the history
## 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
  • Loading branch information
vishalnarkhede authored Oct 3, 2023
1 parent 88ef7ef commit d2ff8ec
Show file tree
Hide file tree
Showing 3 changed files with 47 additions and 1 deletion.
3 changes: 3 additions & 0 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { WSConnectionFallback } from './connection_fallback';
import { isErrorResponse, isWSFailure } from './errors';
import {
addFileToFormData,
axiosParamsSerializer,
chatCodes,
isFunction,
isOnline,
Expand Down Expand Up @@ -305,6 +306,8 @@ export class StreamChat<StreamChatGenerics extends ExtendableGenerics = DefaultG
this.defaultWSTimeoutWithFallback = 6000;
this.defaultWSTimeout = 15000;

this.axiosInstance.defaults.paramsSerializer = axiosParamsSerializer;

/**
* logger function should accept 3 parameters:
* @param logLevel string
Expand Down
14 changes: 14 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import FormData from 'form-data';
import { AscDesc, ExtendableGenerics, DefaultGenerics, OwnUserBase, OwnUserResponse, UserResponse } from './types';
import { AxiosRequestConfig } from 'axios';

/**
* logChatPromiseExecution - utility function for logging the execution of a promise..
Expand Down Expand Up @@ -245,3 +246,16 @@ export function removeConnectionEventListeners(cb: (e: Event) => 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('&');
};
31 changes: 30 additions & 1 deletion test/unit/utils.js
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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);
}
});
});

0 comments on commit d2ff8ec

Please sign in to comment.