I have a React Native app for iOS that successfully uploads images to Google Cloud Storage. When I import the same code into a new Expo app, the upload fails on both the iOS and Android simulators:
const formData = new FormData();
Object.entries({
...data.imageFields, // Includes key, policy, x-goog-algorithm, x-goog-credential, x-goog-signature, and x-goog-meta-source values I already requested from GCS in my back-end API
file: {
uri: curPhoto.path, // "/Users/username/Library/Developer/CoreSimulator/Devices/9EDEBECD-B892-4D34-9142-C559B5A32416/data/Containers/Data/Application/2879C35B-DE2C-439F-8AF4-AA53D6D0C380/Library/Caches/EC6FD27A-A8E6-4344-9580-191130EE7045.jpg"
name: imageId, // "9af64c5f-e0d7-45cf-88a0-4635am46d16e"
type: curPhoto.mimeType, // "image/jpeg"
},
}).forEach(([key, value]) => {
formData.append(key, value as string | Blob);
});
const URL = "https://storage.googleapis.com/my-bucket/";
const result = await fetch(URL, {
method: "POST",
body: formData,
headers: { // Tried both with and without any headers specified
Accept: "application/json",
"Content-Type": "multipart/form-data",
},
});
// Results in 400 network error with no details
Is there anything I need to do differently for Expo? I’m using RN 0.74.5 and Expo 51.0.28.
The culprit was a bug found in a more recent version of React Native. React Native updated the “content-disposition” header to support UTF-8 filenames. However, Google Cloud Storage does not know how to read “filename*=UTF-8” in the header.
Here’s a patch provided by Emre Yilmaz on Bluesky:
type Headers = { [name: string]: string };
type FormDataPart =
| { string: string; headers: Headers }
| {
uri: string;
headers: Headers;
name?: string;
type?: string;
};
// Bug fix for https://github.com/facebook/react-native/issues/44737
export default class FormDataFixed extends FormData {
constructor() {
super();
}
getParts(): Array<FormDataPart> {
// @ts-expect-error
return this._parts.map(([name, value]) => {
const contentDisposition = 'form-data; name="' + name + '"';
const headers: Headers = { "content-disposition": contentDisposition };
// The body part is a "blob", which in React Native just means an object
// with a uri attribute. Optionally, it can also have a 'name' and 'type'
// attribute to specify filename and content type (cf. web Blob interface.)
if (typeof value === "object" && !Array.isArray(value) && value) {
if (typeof value.name === "string") {
headers["content-disposition"] += `; filename="${value.name}"`;
}
if (typeof value.type === "string") {
headers["content-type"] = value.type;
}
return { ...value, headers, fieldName: name };
}
// Convert non-object values to strings as per FormData.append()
return { string: String(value), headers, fieldName: name };
});
}
}
- Relevant React Native issue
- Relevant fix shared on Bluesky