Search code examples
react-nativeexpomulter

Form data of image uploaded as string '[object Object]' when using Expo


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...

Solution

  • 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.

    Option 1: base64 field

    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.

    Option 2: blob encoding

    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
    }