I am trying to upload an image retrieved with Expo's ImagePicker
. Here is my React Native component:
import * as ImagePicker from "expo-image-picker";
const Foo = () => {
const [photoUri, setPhotoUri] = useState("");
const choosePhoto = async () => {
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
allowsEditing: true,
quality: 1,
});
setPhotoUri(result.cancelled ? "" : result.uri);
};
const uploadPhoto = async () => {
if (photoUri == "") {
return;
}
const formData = new FormData();
formData.append("photo", {
uri: photoUri,
name: "test",
type: "image/jpeg",
});
return await fetch(Constants.manifest.extra.UPLOAD_IMAGE_URI, {
method: "POST",
body: formData,
headers: {
// No header otherwise multer will complain about missing boundary
// "content-type": "multipart/form-data",
},
});
};
return (
<View>
<TouchableOpacity onPress={choosePhoto}>
<Text>Choose Photo</Text>
<TouchableOpacity onPress={uploadPhoto}>
<Text>Confirm Image</Text>
</TouchableOpacity>
</TouchableOpacity>
);
};
Here is my Express backend:
import * as express from "express";
import * as multer from "multer";
const app = express();
const fileUpload = multer();
app.post(
"/profile_image/upload",
fileUpload.single("photo"),
async (req, _res, _next) => {
console.log(req.body);
console.log(req.body.photo);
console.log(req.file);
}
);
app.listen(
{
port: 8000,
},
() => {
console.log("Started server!");
}
);
When I test this in the Web version and check my Chrome console, it shows that the request was made where the photo
field is a string '[object Object]'
.
Similarly my Express endpoint parses the field as a string:
[Object: null prototype] { photo: '[object Object]' }
[object Object]
undefined
Also, another weird thing is that the photoUri
returned by ImagePicker
, at least in the Web app, defaults to the base64 encoded version instead of the actual filepath. Not sure if this is intentional:
data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD//gA8Q1JFQVRPUjogZ2QtanBlZyB2MS4wICh1c2luZyBJSkcgSlBFRyB2NjIpLCBxdWFsaXR5ID0gMTAwCv/bAEMAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBA...
Note that the field object with uri, type, name
keys is required when the uri
is not base64 (e.g., on Android/iOS), but the following is required to work with ImagePicker
on web since it returns a base64
encoding.
So I realized since I was uploading a base64 encoding of the image, I need not wrap the photo
field in a dict and instead can just do:
formData.append("photo", photoUri);
Furthermore, since photoUri
is O(1MB), I needed to increase multer
's fieldSize limit to something reasonable (5MB in this case):
const fileUpload = multer({
limits: { fieldSize: 5 * 1024 * 1024 * 1024 },
});
I was able to retrieve the base64 encoding of the image in req.body.photo
subsequently.
The other option is to convert the base64 encoding to a Blob so it works with FormData.
formData.append("photo", dataURItoBlob(photoUri));
Then multer will be able to parse the above as a file without any limit overrides:
app.post(
"/profile_image/upload",
fileUpload.single("photo"),
async (req, _res) => {
console.log("file", req.file);
}
);
file {
fieldname: 'photo',
originalname: 'blob',
encoding: '7bit',
mimetype: 'image/jpeg',
buffer: <Buffer ff d8 ff e0 00 10 4a 46 49 46 00 01 01 00 00 01 00 01 00 00 ff fe 00 3c 43 52 45 41 54 4f 52 3a 20 67 64 2d 6a 70 65 67 20 76 31 2e 30 20 28 75 73 69 ... 2332888 more bytes>,
size: 2332938
}