Search code examples
node.jsreactjsamazon-web-servicesamazon-s3aws-sdk

Trying to retrieve an mp3 file stored in AWS S3 and load it into my React client as a Blob...it's not working


I have a react web app that allows users to record mp3 files in the browser. These mp3 files are saved in an AWS S3 bucket and can be retrieved and loaded back into the react app during the user's next session.

Saving the file works just fine, but when I try to retrieve the file with getObject() and try to create an mp3 blob on the client-side, I get a small, unusable blob:

enter image description here

Here's the journey the recorded mp3 file goes on:

1) Saving to S3

In my Express/Node server, I receive the uploaded mp3 file and save to the S3 bucket:

//SAVE THE COMPLETED AUDIO TO S3
router.post("/", [auth, upload.array('audio', 12)], async (req, res) => {

    try {
        //get file
        const audioFile = req.files[0];

        //create object key
        const userId = req.user;
        const projectId = req.cookies.currentProject;
        const { sectionId } = req.body;
        const key = `${userId}/${projectId}/${sectionId}.mp3`;

        const fileStream = fs.createReadStream(audioFile.path)

        const uploadParams = {
            Bucket: bucketName,
            Body: fileStream,
            Key: key,
            ContentType: "audio/mp3" 
        }

        const result = await s3.upload(uploadParams).promise();
        
        res.send(result.key);

    } catch (error) {
        console.error(error);
        res.status(500).send();
    }
});

As far as I know, there are no problems at this stage. The file ends up in my S3 bucket with "type: mp3" and "Content-Type: audio/mp3".

2) Loading file from S3 Bucket

When the react app is loaded up, an HTTP GET Request is made in my Express/Node server to retrieve the mp3 file from the S3 Bucket

//LOAD A FILE FROM S3
router.get("/:sectionId", auth, async(req, res) => {    

    try {
        //create key from user/project/section IDs
        const sectionId = req.params.sectionId;
        const userId = req.user;
        const projectId = req.cookies.currentProject;
        const key = `${userId}/${projectId}/${sectionId}.mp3`;

        const downloadParams = {
            Key: key,
            Bucket: bucketName
        }

        s3.getObject(downloadParams, function (error, data) {
            if (error) {
                console.error(error);
                res.status(500).send();
            }
            res.send(data);
        });

    } catch (error) {
        console.error(error);
        res.status(500).send();
    }
    
});

The "data" returned here is as such: enter image description here

3) Making a Blob URL on the client

Finally, in the React client, I try to create an 'audio/mp3' blob from the returned array buffer

const loadAudio = async () => {
        
        const res = await api.loadAudio(activeSection.sectionId);
        
        const blob = new Blob([res.data.Body], {type: 'audio/mp3' });
        
        const url = URL.createObjectURL(blob);
                
        globalDispatch({ type: "setFullAudioURL", payload: url });
}

The created blob is severely undersized and appears to be completely unusable. Downloading the file results in a 'Failed - No file' error. enter image description here

I've been stuck on this for a couple of days now with no luck. I would seriously appreciate any advice you can give!

Thanks

EDIT 1

Just some additional info here: in the upload parameters, I set the Content-Type as audio/mp3 explicitly. This is because when not set, the Content-Type defaults to 'application/octet-stream'. Either way, I encounter the same issue with the same result.

EDIT 2 At the request of a commenter, here is the res.data available on the client-side after the call is complete:

enter image description here


Solution

  • Based on the output of res.data on the client, there are a couple of things that you'd need to do:

    • Replace uses of res.data.Body with res.data.Body.data (as the actual data array is in the data attribute of res.data.Body)
    • Pass a Uint8Array to the Blob constructor, as the existing array is of a larger type, which will create an invalid blob

    Putting that together, you would end up replacing:

    const blob = new Blob([res.data.Body], {type: 'audio/mp3' });
    

    with:

    const blob = new Blob([new Uint8Array(res.data.Body.data)], {type: 'audio/mp3' });
    

    Having said all that, the underlying issue is that the NodeJS server is sending the content over as a JSON encoded serialisation of the response from S3, which is likely overkill for what you are doing. Instead, you can send the Buffer across directly, which would involve, on the server side, replacing:

    res.send(data);
    

    with:

    res.set('Content-Type', 'audio/mp3');
    res.send(data.Body);
    

    and on the client side (likely in the loadAudio method) processing the response as a blob instead of JSON. If using the Fetch API then it could be as simple as:

    const blob = await fetch(<URL>).then(x => x.blob());