How to retrieve presigned url for video after successful upload (using AWS S3)

For starters, my stack is React on the front end, with a Rails API using Shrine.

I’m trying to show a preview for an uploaded video before submitting a form. It’s important to emphasize that this is specifically for a video upload, not an image. So the 'thumbnail:generated' event will not apply here.

I can’t seem to find any events that uppy provides that returns a cached video preview (like thumbnail:generated does) or anything that passes back a presigned url for the uploaded file (less expected, obviously), so the only option I see is constructing the url manually. Here’s what I’m currently trying for that (irrelevant code removed for brevity):

import React, { useEffect, useState } from 'react'

import AwsS3 from '@uppy/aws-s3'
import Uppy from '@uppy/core'
import axios from 'axios'
import { DragDrop } from '@uppy/react'

import { API_BASE } from '../../../api'

const constructParams = (metadata) => ([
  `?X-Amz-Algorithm=${metadata['x-amz-algorithm']}`,
  `&X-Amz-Credential=${metadata['x-amz-credential']}`,
  `&X-Amz-Date=${metadata['x-amz-date']}`,
  '&X-Amz-Expires=900',
  '&X-Amz-SignedHeaders=host',
  `&X-Amz-Signature=${metadata['x-amz-signature']}`,
].join('').replaceAll('/', '%2F'))

const MediaUploader = () => {
  const [videoSrc, setVideoSrc] = useState('')

  const uppy = new Uppy({
    meta: { type: 'content' },
    restrictions: {
      maxNumberOfFiles: 1
    },
    autoProceed: true,
  })

  const getPresigned = async (id, type) => {
    const response = await axios.get(`${API_BASE}/s3/params?filename=${id}&type=${type}`)

    const { fields, url } = response.data
    const params = constructParams(fields)
    const presignedUrl = `${url}/${fields.key}${params}`
    console.log('presignedUrl from Shrine request data: ', presignedUrl)
    
    setVideoSrc(presignedUrl)
  }

  useEffect(() => {
    uppy
      .use(AwsS3, {
        id: `AwsS3:${Math.random()}`,
        companionUrl: API_BASE,
      })

    uppy.on('upload-success', (file, _response) => {
      const { type, meta } = file

      // First attempt to construct presigned URL here
      const url = 'https://my-s3-bucket.s3.us-west-1.amazonaws.com'
      const params = constructParams(meta)
      const presignedUrl = `${url}/${meta.key}${params}`
      console.log('presignedUrl from upload-success data: ', presignedUrl)
      
      // Second attempt to construct presigned URL here
      const id = meta.key.split(`${process.env.REACT_APP_ENV}/cache/`)[1]
      getPresigned(id, type)
    })
  }, [uppy])

  return (
    <div className="MediaUploader">
      <div className="Uppy__preview__wrapper">
        <video
          src={videoSrc || ''}
          className="Uppy__preview"
          controls
        />
      </div>
      {(!videoSrc || videoSrc === '') && (
        <DragDrop
          uppy={uppy}
          className="UploadForm"
          locale={{
            strings: {
              dropHereOr: 'Drop here or %{browse}',
              browse: 'browse',
            },
          }}
        />
      )}
    </div>
  )
}

export default MediaUploader

Both urls here come back with a SignatureDoesNotMatch error from AWS.

The manual construction of the url comes mainly from constructParams. I have two different implementations of this, the first of which takes the metadata directly from the uploaded file data in the 'upload-success' event, and then just concatenates a string to build the url. The second one uses getPresigned, which makes a request to my API, which points to a generated Shrine path that should return data for a presigned URL. API_BASE simply points to my Rails API. More info on the generated Shrine route here.

It’s worth noting that everything works perfectly with the upload process that passes through Shrine, and after submitting the form, I’m able to get a presigned url for the video and play it without issue on the site. So I have no reason to believe Shrine is returning incorrectly signed urls.

I’ve compared the two presigned urls I’m manually generating in the form, with the url returned from Shrine after uploading. All 3 are identical in structure, but have different signatures. Here are those three urls:

presignedUrl from upload-success data: 
https://my-s3-bucket.s3.us-west-1.amazonaws.com/development/cache/41b229fb17cbf21925d2cd907a59be25.mp4?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAW63AYCMFA4374OLC%2F20221210%2Fus-west-1%2Fs3%2Faws4_request&X-Amz-Date=20221210T132613Z&X-Amz-Expires=900&X-Amz-SignedHeaders=host&X-Amz-Signature=97aefd1ac7f3d42abd2c48fe3ad50b542742ad0717a51528c35f1159bfb15609

presignedUrl from Shrine request data: 
https://my-s3-bucket.s3.us-west-1.amazonaws.com/development/cache/023592fb14c63a45f02c1ad89a49e5fd.mp4?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAW63AYCMFA4374OLC%2F20221210%2Fus-west-1%2Fs3%2Faws4_request&X-Amz-Date=20221210T132619Z&X-Amz-Expires=900&X-Amz-SignedHeaders=host&X-Amz-Signature=7171ac72f7db2b8871668f76d96d275aa6c53f71b683bcb6766ac972e549c2b3

presigned url displayed on site after form submission:
https://my-s3-bucket.s3.us-west-1.amazonaws.com/development/cache/41b229fb17cbf21925d2cd907a59be25.mp4?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAW63AYCMFA4374OLC%2F20221210%2Fus-west-1%2Fs3%2Faws4_request&X-Amz-Date=20221210T132734Z&X-Amz-Expires=900&X-Amz-SignedHeaders=host&X-Amz-Signature=9ecc98501866f9c5bd460369a7c2ce93901f94c19afa28144e0f99137cdc2aaf

The first two urls come back with SignatureDoesNotMatch, while the third url properly plays the video.

I’m aware the first and third urls have the same file name, while the second url does not. I’m not sure what to make of that, though, but the relevance of this is secondary to me, since that solution was more of a last ditch effort anyway.

I’m not at all attached to the current way I’m doing things. It’s just the only solution I could come up with, due to lack of options. If there’s a better way of going about this, I’m very open to suggestions.

Thanks for reading!