Closed statico closed 2 months ago
Hey @statico, can you please help me with the version of Axios that is been used in your project? Ideally, we haven't seen this reported yet by any of our customers, but I suspect it is the Axios version that is used in your project that could be leading to this issue for you.
@khushal87 Sure:
dependencies:
stream-chat-expo 5.18.1
βββ¬ stream-chat-react-native-core 5.18.1
βββ¬ stream-chat 8.12.3
βββ axios 0.22.0
I'll see if i can upgrade the axios transitive dependency and see if that makes a difference.
Looking at your Axios version it looks like that's a problem. In stream-chat-js
, the client that we use for all the network stuff uses 1.6.0, which in your case is 0.22.0 which could be a culprit.
Also, you are using an old version of stream-chat-expo. Any reasons for that?
@statico you mentioned in the issue that you use stream-chat-expo 5.26.0
but here it seems like you are using 5.18.1
. Could you confirm the version please
this issue was fixed in https://github.com/GetStream/stream-chat-react-native/pull/2334
v5.22.1
I had tried upgrading to stream-chat-expo 5.26.0
but that didn't have any affect for me. I will try upgrading again as well as verifying the Axios versions.
I've confirmed this is still an issue with stream-chat-expo 5.26.0
and axios 1.6.8
. I removed all node_modules
directories and ran expo start --clean
to be sure.
$ pnpm why axios
Legend: production dependency, optional only, dev only
...
dependencies:
stream-chat-expo 5.26.0
βββ¬ stream-chat-react-native-core 5.26.0
βββ¬ stream-chat 8.17.0
βββ axios 1.6.8
@statico since you are using prnpm here you could be resolving to older axios.. due to the monorepo structure
Could you please give me add this to your metro config before exporting your config and give us what is logged please
I suspect that metro is still resolving to older axios
config.resolver.resolveRequest = (context, moduleName, platform) => {
const resolved = context.resolveRequest(context, moduleName, platform);
if (
moduleName.startsWith('axios') &&
context.originModulePath.includes('stream-chat')
) {
console.log("axios resolution", { resolved });
}
return resolved;
};
module.exports = config;
Ah ha! That resolution reported:
axios resolution {
resolved: {
type: 'sourceFile',
filePath: '/Users/ian/dev/xxxx/app/node_modules/axios/index.js'
}
}
And digging around showed that you're correct, the wrong version is being resolved:
$ cat /Users/ian/dev/xxxx/app/node_modules/axios/lib/env/data.js
module.exports = {
"version": "0.27.2"
};
pnpm still said I had 1.6.8 installed despite this:
$ pnpm why axios
Legend: production dependency, optional only, dev only
@xxxx/mobile@1.0.0 /Users/ian/dev/xxxx/app/packages/mobile
dependencies:
stream-chat-expo 5.27.1
βββ¬ stream-chat-react-native-core 5.27.1
βββ¬ stream-chat 8.17.0
βββ axios 1.6.8
So I explicitly installed Axios within this subproject using pnpm add axios@latest
, and now the resolution shows:
axios resolution {
resolved: {
type: 'sourceFile',
filePath: '/Users/ian/dev/xxxx/app/packages/mobile/node_modules/axios/index.js'
}
And that is definitely a newer version:
$ cat /Users/ian/dev/xxxx/app/packages/mobile/node_modules/axios/lib/env/data.js
export const VERSION = "1.6.8";
However, I did this:
node_modules
dirspnpm install
expo start --clean
And I'm still experiencing the bug:
Hey @statico, by any chance do you have any customization on the sendImage logic of our SDK in your app? We are not able to reproduce this issue on our environments. May be a reproducible repo would help us facilitate this issue.
Nope, no sendImage
customization. :/
Hey @statico, we were not able to reproduce the problem on our side in the Expo 49 environment. Please provide us with a minimum reproducible repository to facilitate this. Thanks π
Closing the issue due to inactivity. Feel free to reopen it if its still relevant with details on how to reproduce it. Thanks π
OK. I'm limited on time but I'll reopen this if I'm ever able to make a standalone reproduction.
I'm having the same issue when trying to upload an image. @khushal87 do you have any suggestions on how to fix it?
axios@1.7.2
node_modules/axios
axios@"^1.6.0" from the root project
axios@"^1.6.0" from stream-chat@8.37.0
node_modules/stream-chat
stream-chat@"^8.37.0" from the root project
peer stream-chat@"^8.33.1" from stream-chat-react@11.23.3
node_modules/stream-chat-react
stream-chat-react@"^11.23.3" from the root project
@alex-mironov I have a workaround in the description using Axios interceptors. It's been working for us with that fix since I filed this issue.
Issue
When uploading an image from the message input in this environment, the upload fails:
Stream hides the error, unfortunately. If you run
adb logcat
you can see this error:(It would be great if the
handleFileOrImageUploadError()
function inMessageInputContext.tsx
showed this error instead of consuming it and hiding it completely.)The only reference to this appears to be https://github.com/facebook/react-native/issues/25244 which, luckily references the solution: https://github.com/facebook/react-native/issues/25244#issuecomment-1826023980
Here's my solution using Axios interceptors:
Steps to reproduce
Steps to reproduce the behavior:
adb logcat
to see errorsExpected behavior
The image should be uploaded to the message input and users should be able to send the image to the channel.
Project Related Information
Customization
Click To Expand
- [MessageInputContext.tsx - uploadFile()](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/contexts/messageInputContext/MessageInputContext.tsx#L983-L1019) ```tsx const uploadFile = async ({ newFile }: { newFile: FileUpload }) => { const { file, id } = newFile; setFileUploads(getUploadSetStateAction(id, FileState.UPLOADING)); let response: Partial = {};
try {
if (value.doDocUploadRequest) {
response = await value.doDocUploadRequest(file, channel);
} else if (channel && file.uri) {
uploadAbortControllerRef.current.set(
file.name,
client.createAbortControllerForNextRequest(),
);
// Compress images selected through file picker when uploading them
if (file.mimeType?.includes('image')) {
const compressedUri = await compressedImageURI(file, value.compressImageQuality);
response = await channel.sendFile(compressedUri, file.name, file.mimeType);
} else {
response = await channel.sendFile(file.uri, file.name, file.mimeType);
}
uploadAbortControllerRef.current.delete(file.name);
}
const extraData: Partial = { thumb_url: response.thumb_url, url: response.file };
setFileUploads(getUploadSetStateAction(id, FileState.UPLOADED, extraData));
} catch (error: unknown) {
if (
error instanceof Error &&
(error.name === 'AbortError' || error.name === 'CanceledError')
) {
// nothing to do
uploadAbortControllerRef.current.delete(file.name);
return;
}
handleFileOrImageUploadError(error, false, id);
}
};
const uploadImage = async ({ newImage }: { newImage: ImageUpload }) => {
const { file, id } = newImage || {};
if (!file) {
return;
}
let response = {} as SendFileAPIResponse;
const uri = file.uri || '';
const filename = file.name ?? uri.replace(/^(file:\/\/|content:\/\/)/, '');
try {
const compressedUri = await compressedImageURI(file, value.compressImageQuality);
const contentType = lookup(filename) || 'multipart/form-data';
if (value.doImageUploadRequest) {
response = await value.doImageUploadRequest(file, channel);
} else if (compressedUri && channel) {
if (value.sendImageAsync) {
uploadAbortControllerRef.current.set(
filename,
client.createAbortControllerForNextRequest(),
);
channel.sendImage(compressedUri, filename, contentType).then(
(res) => {
uploadAbortControllerRef.current.delete(filename);
if (asyncIds.includes(id)) {
// Evaluates to true if user hit send before image successfully uploaded
setAsyncUploads((prevAsyncUploads) => {
prevAsyncUploads[id] = {
...prevAsyncUploads[id],
state: FileState.UPLOADED,
url: res.file,
};
return prevAsyncUploads;
});
} else {
const newImageUploads = getUploadSetStateAction(
id,
FileState.UPLOADED,
{
url: res.file,
},
);
setImageUploads(newImageUploads);
}
},
() => {
uploadAbortControllerRef.current.delete(filename);
},
);
} else {
uploadAbortControllerRef.current.set(
filename,
client.createAbortControllerForNextRequest(),
);
response = await channel.sendImage(compressedUri, filename, contentType);
uploadAbortControllerRef.current.delete(filename);
}
}
if (Object.keys(response).length) {
const newImageUploads = getUploadSetStateAction(id, FileState.UPLOADED, {
height: file.height,
url: response.file,
width: file.width,
});
setImageUploads(newImageUploads);
}
} catch (error) {
if (
error instanceof Error &&
(error.name === 'AbortError' || error.name === 'CanceledError')
) {
// nothing to do
uploadAbortControllerRef.current.delete(filename);
return;
}
handleFileOrImageUploadError(error, true, id);
}
};
```
- [client.ts - sendFile()](https://github.com/GetStream/stream-chat-js/blob/master/src/client.ts#L1040-L1058)
```tsx
sendFile(
url: string,
uri: string | NodeJS.ReadableStream | Buffer | File,
name?: string,
contentType?: string,
user?: UserResponse,
) {
const data = addFileToFormData(uri, name, contentType || 'multipart/form-data');
if (user != null) data.append('user', JSON.stringify(user));
return this.doAxiosRequest('postForm', url, data, {
headers: data.getHeaders ? data.getHeaders() : {}, // node vs browser
config: {
timeout: 0,
maxContentLength: Infinity,
maxBodyLength: Infinity,
},
});
}
```
- [client.ts - doAxiosRequest()](https://github.com/GetStream/stream-chat-js/blob/master/src/client.ts#L958)
```tsx
doAxiosRequest = async (
type: string,
url: string,
data?: unknown,
options: AxiosRequestConfig & {
config?: AxiosRequestConfig & { maxBodyLength?: number };
} = {},
): Promise => {
await this.tokenManager.tokenReady();
const requestConfig = this._enrichAxiosOptions(options);
try {
let response: AxiosResponse;
this._logApiRequest(type, url, data, requestConfig);
switch (type) {
case 'get':
response = await this.axiosInstance.get(url, requestConfig);
break;
case 'delete':
response = await this.axiosInstance.delete(url, requestConfig);
break;
case 'post':
response = await this.axiosInstance.post(url, data, requestConfig);
break;
case 'postForm':
response = await this.axiosInstance.postForm(url, data, requestConfig);
break;
case 'put':
response = await this.axiosInstance.put(url, data, requestConfig);
break;
case 'patch':
response = await this.axiosInstance.patch(url, data, requestConfig);
break;
case 'options':
response = await this.axiosInstance.options(url, requestConfig);
break;
default:
throw new Error('Invalid request type');
}
this._logApiResponse(type, url, response);
this.consecutiveFailures = 0;
return this.handleResponse(response);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (e: any /**TODO: generalize error types */) {
e.client_request_id = requestConfig.headers?.['x-client-request-id'];
this._logApiError(type, url, e);
this.consecutiveFailures += 1;
if (e.response) {
/** connection_fallback depends on this token expiration logic */
if (e.response.data.code === chatCodes.TOKEN_EXPIRED && !this.tokenManager.isStatic()) {
if (this.consecutiveFailures > 1) {
await sleep(retryInterval(this.consecutiveFailures));
}
this.tokenManager.loadToken();
return await this.doAxiosRequest(type, url, data, options);
}
return this.handleResponse(e.response);
} else {
throw e as AxiosError;
}
}
};
```
Offline support
Environment
Click To Expand
#### `package.json`: ```json { "dependencies": { "@clerk/clerk-expo": "0.19.16", "@expo/webpack-config": "19.0.0", "@fortawesome/fontawesome-svg-core": "6.4.2", "@fortawesome/free-brands-svg-icons": "6.4.2", "@fortawesome/pro-light-svg-icons": "6.4.2", "@fortawesome/pro-regular-svg-icons": "6.4.2", "@fortawesome/pro-solid-svg-icons": "6.4.2", "@fortawesome/react-native-fontawesome": "0.3.0", "@gorhom/bottom-sheet": "4.5.1", "@react-native-anywhere/polyfill-base64": "0.0.1-alpha.0", "@react-native-async-storage/async-storage": "1.19.3", "@react-native-community/datetimepicker": "7.6.0", "@react-native-community/netinfo": "9.4.1", "@sentry/react": "7.73.0", "@sentry/react-native": "5.10.0", "@tanstack/react-query": "4.35.7", "@trpc/client": "10.38.5", "@trpc/react-query": "10.38.5", "change-case": "4.1.2", "dotenv": "16.3.1", "expo": "49.0.13", "expo-application": "5.4.0", "expo-auth-session": "5.2.0", "expo-av": "13.6.0", "expo-camera": "13.6.0", "expo-clipboard": "4.5.0", "expo-constants": "14.4.2", "expo-contacts": "12.4.0", "expo-crypto": "12.6.0", "expo-dev-client": "2.4.11", "expo-device": "5.6.0", "expo-document-picker": "11.7.0", "expo-file-system": "15.6.0", "expo-font": "11.6.0", "expo-haptics": "12.6.0", "expo-image": "1.5.1", "expo-image-manipulator": "11.5.0", "expo-image-picker": "14.5.0", "expo-linear-gradient": "~12.3.0", "expo-linking": "5.0.2", "expo-localization": "14.5.0", "expo-location": "16.3.0", "expo-media-library": "15.6.0", "expo-network": "5.6.0", "expo-notifications": "0.20.1", "expo-router": "2.0.8", "expo-secure-store": "12.5.0", "expo-sharing": "11.7.0", "expo-splash-screen": "0.20.5", "expo-status-bar": "1.7.1", "expo-store-review": "6.6.0", "expo-task-manager": "11.5.0", "expo-updates": "0.18.14", "expo-web-browser": "12.5.0", "formik": "2.4.5", "intl-pluralrules": "2.0.1", "json-stringify-safe": "5.0.1", "just-compare": "2.3.0", "libphonenumber-js": "1.10.45", "lodash.debounce": "4.0.8", "luxon": "3.4.3", "metro": "0.79.1", "metro-resolver": "0.79.1", "metro-runtime": "0.79.1", "ms": "2.1.3", "p-retry": "6.1.0", "pluralize": "8.0.0", "posthog-react-native": "2.7.1", "react": "18.2.0", "react-content-loader": "6.2.1", "react-dom": "18.2.0", "react-error-boundary": "4.0.11", "react-native": "0.72.5", "react-native-date-picker": "4.3.3", "react-native-dialog": "9.3.0", "react-native-draggable-flatlist": "4.0.1", "react-native-flex-layout": "0.1.5", "react-native-gesture-handler": "2.13.1", "react-native-keyboard-aware-scroll-view": "0.9.5", "react-native-maps": "1.7.1", "react-native-mmkv": "^2.12.1", "react-native-popup-menu": "0.16.1", "react-native-reanimated": "3.5.4", "react-native-reanimated-confetti": "1.0.1", "react-native-restart": "0.0.27", "react-native-safe-area-context": "4.7.2", "react-native-screens": "3.25.0", "react-native-svg": "13.14.0", "react-native-swipe-list-view": "3.2.9", "react-native-web": "0.19.9", "react-native-web-swiper": "2.2.4", "react-native-webview": "13.6.0", "react-test-renderer": "18.2.0", "recoil": "0.7.7", "rn-range-slider": "2.2.2", "sentry-expo": "7.0.1", "stream-chat-expo": "5.18.1", "swr": "2.2.4", "yup": "1.3.2" }, "devDependencies": { "@babel/core": "7.23.0", "@babel/plugin-transform-flow-strip-types": "7.22.5", "@clerk/types": "3.53.0", "@testing-library/jest-dom": "6.1.3", "@testing-library/jest-native": "5.4.3", "@testing-library/react": "14.0.0", "@testing-library/react-native": "12.3.0", "@types/lodash.debounce": "4.0.7", "@types/ms": "0.7.32", "@types/react": "18.2.24", "@types/react-native": "0.72.3", "@types/webpack-env": "1.18.2", "@typescript-eslint/eslint-plugin": "6.7.4", "@typescript-eslint/parser": "6.7.4", "eslint": "8.50.0", "eslint-config-prettier": "9.0.0", "eslint-plugin-import": "2.28.1", "eslint-plugin-react": "7.33.2", "eslint-plugin-simple-import-sort": "10.0.0", "jest-expo": "49.0.0", "knip": "^5.0.2", "typescript": "5.2.2" } } ``` **`react-native info` output:** n/a -- using Expo - **Platform that you're experiencing the issue on**: - [ ] iOS - [x] Android - [ ] **iOS** but have not tested behavior on Android - [ ] **Android** but have not tested behavior on iOS - [ ] Both - **`stream-chat-expo` version you're using that has this issue:** - 5.18.1 and 5.26.0 - Device/Emulator info: - [x] I am using a physical device - OS version: `Android 14` - Device/Emulator: `Pixel 5a`
Additional context
Screenshots
Click To Expand
(see above video)