How to handle result of liveview direct upload to s3

Hi,

I’m writing an application that will do a direct upload to s3 using a presigned url using the live_view uploads, and if successful I need to persist the file location in my DB.

I can successfully upload a file, but am struggling with getting feedback about the result of the upload.

Things I have tried:

  • Using upload_errors/2 in the template. This is good for displaying the errors, and updating the markdown if something fails but I need to know on the server.

  • Having phx-submit event on the form, this is fired when the upload is ‘complete’, but the socket when this is called doesn’t seem to contain any errors. Looking at the socket.assigns.uploads when I’ve fed an invalid url shows that the progress for the file is at 100, but there are no errors recorded (its an empty list). The front end does acknowledge the error in this case.

It seems to me that there should be a built in event that fires when an upload is resolved, but I am at a loss for finding what that is. I’m hoping that either someone could point out what I’m missing or point me in a direction for a fix.

Thanks for any feedback.

@MarkZsombor

You may be able to try getting the errors in the progress/3 function in allow_upload/3.

Perhaps something like this:

...
socket
|> allow_uploads(:file,
    ...,
    progress: &handle_progress/3
)
...
defp handle_progress(:file, entry, socket) do
    errors = for {ref, error} <- socket.assigns.uploads.file.errors, ref == entry.ref, do: error
    # Handle errors here
    {:noreply, socket}
end

Unfortunately I’m not sure if the progress function is guaranteed to be called when an error occurs.

That doesn’t seem to help, I’m going to paste my uploader in case the issue is there. I’ve noticed if I force the error by adding an entry.error() call I do see an error in the handle_progress function as above, but the onload and onerrror listeners for xhr don’t seem to be doing anything.

let Uploaders = {}

Uploaders.S3 = function(entries, onViewError){
  entries.forEach(entry => {
    let {url} = entry.meta;
    const xhr = new XMLHttpRequest();
    onViewError(() => xhr.abort());
    xhr.onload = () => xhr.status === 204 || entry.error();
    xhr.onerror = () => entry.error();
    xhr.upload.addEventListener("progress", (event) => {
      if(event.lengthComputable){
        let percent = Math.round((event.loaded / event.total) * 100);
        entry.progress(percent);
      }
    })
    xhr.open("put", url, true);
    xhr.send(entry.file);
  })
}

@MarkZsombor

Hm I seem to be able to get an error come through consistently, whether that be be turning off my internet connection or by modifying the url to something invalid.

errors is coming through as [:external_client_failure] in both cases.

defp handle_progress(:file, entry, socket) do
    errors = for {ref, error} <- socket.assigns.uploads.file.errors, ref == entry.ref, do: error
    if entry.done? and errors == [] do
      consume_uploaded_entry(socket, entry, fn %{} = meta ->
      ...

There are a few differences with my Uploader which may help.

  xhr.onload = () => xhr.status === 200 ? entry.progress(100) : entry.error()
  xhr.onerror = () => entry.error()
  xhr.upload.addEventListener("progress", (event) => {
    if (event.lengthComputable) {
      let percent = Math.round((event.loaded / event.total) * 100)
      if (percent < 100) { entry.progress(percent) }
    }
  })

We’re relying on the onload to report completion rather than the progress event.

I was still seeing errors come through when I used your version, the main difference was that I was getting a 100% completion, a false positive, which was promptly followed by the error.

Credit for this goes to this issue: External upload not finished when `consume_uploaded_entries` is called · Issue #1320 · phoenixframework/phoenix_live_view · GitHub, and the documentation appears to have been updated for the next release.

(Also I’m expecting a 200 in my case because I’m using DigitalOcean spaces which is giving me a 200 instead of a 204).