16

Been trying with no luck to upload an image to S3 from React Native using pre-signed url. Here is my code:

generate pre-signed url in node:

const s3 = new aws.S3();

const s3Params = {
  Bucket: bucket,
  Key: fileName,
  Expires: 60,
  ContentType: 'image/jpeg',  
  ACL: 'public-read'
};

return s3.getSignedUrl('putObject', s3Params);

here is RN request to S3:

var file = {
  uri: game.pictureToSubmitUri,
  type: 'image/jpeg',
  name: 'image.jpg',
};

const xhr = new XMLHttpRequest();
var body = new FormData();
body.append('file', file);
xhr.open('PUT', signedRequest);
xhr.onreadystatechange = () => {
  if(xhr.readyState === 4){
    if(xhr.status === 200){
      alert('Posted!');
    }
    else{
      alert('Could not upload file.');
   }
 }
};
xhr.send(body);

game.pictureToSubmitUri = assets-library://asset/asset.JPG?id=A282A2C5-31C8-489F-9652-7D3BD5A1FAA4&ext=JPG

signedRequest = https://my-bucket.s3-us-west-1.amazonaws.com/8bd2d4b9-3206-4bff-944d-e06f872d8be3?AWSAccessKeyId=AKIAIOLHQY4GAXN26FOQ&Content-Type=image%2Fjpeg&Expires=1465671117&Signature=bkQIp5lgzuYrt2vyl7rqpCXPcps%3D&x-amz-acl=public-read

Error message:

<Code>SignatureDoesNotMatch</Code>
<Message>
The request signature we calculated does not match the signature you provided. Check your key and signing method.
</Message>

I can successfully curl and image to S3 using the generated url, and I seem to be able to successfully post to requestb.in from RN (however I can only see the raw data on requestb.in so not 100% sure the image is properly there).

Based on all this, I've narrowed my issue down to 1) my image is not correctly uploading period, or 2) somehow the way S3 wants my request is different then how it is coming in.

Any help would be muuuuuucchhhh appreciated!

UPDATE

Can successfully post from RN to S3 if body is just text ({'data': 'foo'}). Perhaps AWS does not like mutliform data? How can I send as just a file in RN???

Cole
  • 838
  • 1
  • 8
  • 25
  • Not sure why your signature is invalid. I have almost the same signing code and it works fine. Your empty successful uploads are due to you passing a path as an URI. `file:///var/.../4BBAE22E-DADC-4240-A266-8E469C0636B8.jpg` should work. – Daniel Basedow Jun 11 '16 at 08:46
  • Does your AWS Secret Key have any trailing forwarding "/"? – Piyush Patil Jun 11 '16 at 11:17
  • @DanielBasedow I don't think the signature is invalid. I can curl to upload images using it. I think something is wrong with how my RN request is being formed? – Cole Jun 12 '16 at 04:02
  • @Cole Just wondering if this is the best practice? When user on client wants to upload a picture I should send filename to Node.js server to generate presigned url -> send presigned url back to client app -> then client app uploads to presigned url? – atkayla Oct 12 '18 at 03:10
  • 1
    @kayla my understanding is direct uploads from client to S3 is the preferred method yes. Sending files through backend creates unnecessary load on servers. Here are the heroku docs: https://devcenter.heroku.com/articles/s3-upload-node – Cole Oct 12 '18 at 18:18

8 Answers8

19

To upload pre-signed S3 URL on both iOS and Android use react-native-blob-util lib

Code snippet:

import RNBlobUtil from 'react-native-blob-util'

const preSignedURL = 'pre-signed url'
const pathToImage = '/path/to/image.jpg' // without file:// scheme at the beginning
const headers = {}

RNBlobUtil.fetch('PUT', preSignedURL, headers, RNBlobUtil.wrap(pathToImage))

Edited 19 Oct 2022 and swapped unsupported RN Fetch Blob for React Native Blob Util package.

Peter Machowski
  • 437
  • 5
  • 11
17

FormData will create a multipart/form-data request. S3 PUT object needs its request body to be a file.

You just need to send your file in the request body without wrapping it into FormData:

function uploadFile(file, signedRequest, url) {
  const xhr = new XMLHttpRequest();
  xhr.open('PUT', signedRequest);
  xhr.onreadystatechange = function() {
    if (xhr.readyState === 4) {
      if(xhr.status === 200) {
        alert(url);
      } else {
        alert('Could not upload file.');
      }
    }
  };
  xhr.send(file);
};

See https://devcenter.heroku.com/articles/s3-upload-node for example in a browser. Please also ensure your Content-Type header is matched with the signed URL request.

Edward Samuel
  • 3,846
  • 1
  • 22
  • 39
  • 2
    Can you elaborate on ensuring that the Content-Type header is matched with the signed URL request? I do not quite understand what you mean by this. – Inchoon Park Aug 24 '16 at 07:50
  • 1
    You just need to set `xhr.setRequestHeader('Content-Type', fileType)` – Abner Terribili Jun 22 '17 at 12:04
  • I think S3 `PUT` requires `Content-Length` [see docs](http://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectPUT.html), whereas S3 `POST` requires a `file` object [see docs](http://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectPOST.html). – Marklar Jul 24 '17 at 10:50
  • 1
    @edward-samuel, I am wondering how to achieve the same with Fetch instead of XMLHttpRequest – leomayleomay Jun 24 '18 at 09:49
  • This code worked for me on iOS, but not on Android. I get xhr.status = 0 but nothing is uploaded to S3. – Xavi A. May 16 '19 at 09:10
6
"rn-fetch-blob": 0.12.0,
"react-native": 0.61.5

This code works for both Android & iOS

const response = await RNFetchBlob.fetch(
  'PUT',
  presignedUrl,
  {
    'Content-Type': undefined
  },
  RNFetchBlob.wrap(file.path.replace('file://', '')),
)

Note {'Content-Type': undefined} is needed for iOS

siriwatknp
  • 203
  • 2
  • 8
  • Using RNFetchBlob is simplest solution for me. Also specifying `Content-Type` = undefined is of course important. – samthui7 Nov 14 '22 at 08:19
0

sorry if none worked for any body. took me 5 days to get this to work . 5 crazy days of no result until my sleepy eyes turned green after little nap. Guess i had a sweet dream that brought the idea. so quickly say u have an end point on ur server to generate the sign url for the request from react native end or from react side or any web frontier. i would be doing this for both react native and react(can serve for html pages and angular pages).

WEB APPROACH

UPLOAD IMAGE TO S3 BUCKET PRESIGNED URI

/*
      Function to carry out the actual PUT request to S3 using the signed request from the app.
    */
    function uploadFile(file, signedRequest, url){
     // document.getElementById('preview').src = url; // THE PREVIEW PORTION
        //    document.getElementById('avatar-url').value = url; //
      const xhr = new XMLHttpRequest();
      xhr.open('PUT', signedRequest);
      xhr.onreadystatechange = () => {
        if(xhr.readyState === 4){
          if(xhr.status === 200){
            document.getElementById('preview').src = url;
           // document.getElementById('avatar-url').value = url;
          }
          else{
            alert('Could not upload file.');
          }
        }
      };
      xhr.send(file);
    }

    /*
      Function to get the temporary signed request from the app.
      If request successful, continue to upload the file using this signed
      request.
    */
    function getSignedRequest(file){
      const xhr = new XMLHttpRequest();

      xhr.open('GET', 'http://localhost:1234'+`/sign-s3?file-name=${file.name}&file-type=${file.type}`);
        xhr.setRequestHeader('Access-Control-Allow-Headers', '*');
    xhr.setRequestHeader('Content-type', 'application/json');
    xhr.setRequestHeader('Access-Control-Allow-Origin', '*');
      xhr.onreadystatechange = () => {
        if(xhr.readyState === 4){
          if(xhr.status === 200){
            const response = JSON.parse(xhr.responseText);
            uploadFile(file, response.signedRequest, response.url);
          }
          else{
            alert('Could not get signed URL.');
          }
        }
      };
      xhr.send();
    }

    /*
     Function called when file input updated. If there is a file selected, then
     start upload procedure by asking for a signed request from the app.
    */
    function initUpload(){
      const files = document.getElementById('file-input').files;
      const file = files[0];
      if(file == null){
        return alert('No file selected.');
      }
      getSignedRequest(file);
    }

    /*
     Bind listeners when the page loads.
    */


   //check if user is actually on the profile page
//just ensure that the id profile page exist  on your html
  if (document.getElementById('profile-page')) {
    document.addEventListener('DOMContentLoaded',() => {

      ///here is ur upload trigger bttn effect

        document.getElementById('file-input').onchange = initUpload;
    });

  }



0

FOR REACT NATIVE I WILL NOT BE USING ANY 3RD PARTY LIBS.

i have my pick image function that picks the image and upload using xhr

const pickImage = async () => {
    let result = await ImagePicker.launchImageLibraryAsync({
     // mediaTypes: ImagePicker.MediaTypeOptions.All,
      allowsEditing: true,
      aspect: [4, 3],
      quality: 1,
      base64:true
    });

    console.log(result);






    if (!result.cancelled) {
     // setImage(result.uri);
      let base64Img = `data:image/jpg;base64,${result.uri}`;




       // ImagePicker saves the taken photo to disk and returns a local URI to it
  let localUri = result.uri;
  let filename = localUri.split('/').pop();

  // Infer the type of the image
  let match = /\.(\w+)$/.exec(filename);
  let type = match ? `image/${match[1]}` : `image`;

  // Upload the image using the fetch and FormData APIs
  let formData = new FormData();
  // Assume "photo" is the name of the form field the server expects
  formData.append('file', { uri: base64Img, name: filename, type });

  const xhr = new XMLHttpRequest();


  xhr.open('GET', ENVIRONMENTS.CLIENT_API+`/sign-s3?file-name=${filename}&file-type=${type}`);
  xhr.setRequestHeader('Access-Control-Allow-Headers', '*');
xhr.setRequestHeader('Content-type', 'application/json');
// xhr.setRequestHeader('Content-type', 'multipart/form-data');
xhr.setRequestHeader('Access-Control-Allow-Origin', '*');
 xhr.setRequestHeader('X-Amz-ACL', 'public-read') //added
xhr.setRequestHeader('Content-Type', type) //added
xhr.onreadystatechange = () => {
  if(xhr.readyState === 4){
    if(xhr.status === 200){
      const response = JSON.parse(xhr.responseText);
      alert(JSON.stringify( response.signedRequest, response.url))
      // uploadFile(file, response.signedRequest, response.url);
      // this.setState({imagename:file.name})
      const xhr2 = new XMLHttpRequest();

            xhr2.open('PUT', response.signedRequest);
            xhr2.setRequestHeader('Access-Control-Allow-Headers', '*');
            xhr2.setRequestHeader('Content-type', 'application/json');
            // xhr2.setRequestHeader('Content-type', 'multipart/form-data');
            xhr2.setRequestHeader('Access-Control-Allow-Origin', '*');
            //  xhr2.setRequestHeader('X-Amz-ACL', 'public-read') //added
            xhr2.setRequestHeader('Content-Type', type) //added
            xhr2.onreadystatechange = () => {
              if(xhr2.readyState === 4){
                if(xhr2.status === 200){

                  alert("successful upload ")
                }
                else{
                  // alert('Could not upload file.');
                  var error = new Error(xhr.responseText)
                  error.code = xhr.status;
                  for (var key in response) error[key] = response[key]
                  alert(error)
                }
              }
            };
            xhr2.send( result.base64)
    }
    else{
      alert('Could not get signed URL.');
    }
  }
};
xhr.send();








    }


  };






then some where in the render method

<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
      <Button title="Pick an image from camera roll" onPress={pickImage} />
      {image && <Image source={{ uri: image }} style={{ width: 200, height: 200 }} />}
    </View>


hope it helps any one who doesnt want sleepless nights like me.
0
import React from 'react'
import { Button, SafeAreaView } from 'react-native'
import { launchImageLibrary } from 'react-native-image-picker'

const Home = () => {

  const getImageFromLibrary = async () => {
    const result = await launchImageLibrary()

    const { type, uri } = result.assets[0]

    const blob = await new Promise((resolve, reject) => {
      const xhr = new XMLHttpRequest()
      xhr.onload = function () {
        resolve(xhr.response)
      }
      xhr.onerror = function () {
        reject(new TypeError('Network request failed'))
      }
      xhr.responseType = 'blob'
      xhr.open('GET', uri, true)
      xhr.send(null)
    })

    // Send your blob off to the presigned url
    const res = await axios.put(presignedUrl, blob)
  }

  return (
    <SafeAreaView>
      <Button onPress={getImageFromLibrary} title="Get from library" />    
    </SafeAreaView>
  )
}

export default Home

Your BE that creates the pre-signed url can look something like this (pseudo code):

const { getSignedUrl } = require('@aws-sdk/s3-request-presigner')
const { S3Client, PutObjectCommand } = require('@aws-sdk/client-s3')

const BUCKET_NAME = process.env.BUCKET_NAME
const REGION = process.env.AWS_REGION

const s3Client = new S3Client({
  region: REGION
})

const body = JSON.parse(request.body)
const { type } = body

const uniqueName = uuidv4()
const date = moment().format('MMDDYYYY')
const fileName = `${uniqueName}-${date}`

const params = {
  Bucket: BUCKET_NAME,
  Key: fileName,
  ContentType: type
}

try {
  const command = new PutObjectCommand(params)
  const signedUrl = await getSignedUrl(s3Client, command, {
    expiresIn: 60
  })

  response.send({ url: signedUrl, fileName })
} catch (err) {
  console.log('ERROR putPresignedUrl : ', err)
  response.send(err)
}

I am using aws-sdk v3 which is nice because the packages are smaller. I create a filename on the BE and send it to the FE. For the params, you don't need anything listed then those 3. Also, I never did anything with CORS and my bucket is completely private. Again, the BE code is pseudo code ish so you will need to edit a few spots.

Lastly, trying to use the native fetch doesn't work. It's not the same fetch you use in React. Use XHR request like I showed else you cannot create a blob.

user2465134
  • 8,793
  • 5
  • 32
  • 46
0

First, install two libraries, then the image convert into base64 after that arrayBuffer, then upload it

import RNFS from 'react-native-fs';
import {decode} from 'base64-arraybuffer';

 try {
        RNFS.readFile(fileUri, 'base64').then(data => {
          const arrayBuffer = decode(data);
          axios
            .put(sThreeApiUrl.signedUrl, arrayBuffer, {
              headers: {
                'Content-Type': 'image/jpeg',
                'Content-Encoding': 'base64',
              },
            })
            .then(res => {
              if (res.status == 200) {
               console.log('image is uploaded successfully');              
              }
            });
        });
      } catch (error) {
        console.log('this is error', error);              }
Ram Sagar
  • 5
  • 3
0

Android status code 0 issue is fixed by putting setrequestheader before onreadystatechange

const xhr = new XMLHttpRequest();
xhr.open('PUT', presignedUrl);
xhr.setRequestHeader('Content-Type', photo.type)
xhr.onreadystatechange = function() {
  if (xhr.readyState === 4) {
    if (xhr.status === 200) {
      alert("file uploaded")
    } else {
      alert('Could not upload file.');
    }
  }
};
xhr.send(file);