Phoenix LiveView External Upload Cancellation

2 min read

While implementing a LiveView external upload to S3 I noticed that the default cancellation mechanism doesn't properly terminate the upload. The upload is cancelled within the LiveView state, but the browser will continue to upload the file via XMLHttpRequest and never gets terminated. When uploading large files or handling file processing automatically on upload via Lambda or this is especially problematic. Thankfully, the fix is very simple.

Below is the JS uploader example from the docs with a few lines added to handle cancellation of the xhr request.

let Uploaders = {};
Uploaders.S3 = function (entries, onViewError) {
  entries.forEach((entry) => {
    let formData = new FormData();
    let { url, fields } = entry.meta;
    Object.entries(fields).forEach(([key, val]) => formData.append(key, val));
    formData.append("file", entry.file);
    let xhr = new XMLHttpRequest();
    onViewError(() => xhr.abort());
    xhr.onload = () =>
      xhr.status === 204 ? 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);
        }
      }
    });

    // ADD THESE LINES

    abortUpload = () => xhr.abort();
    window.addEventListener(`phx:cancel-${entry.ref}`, abortUpload, {
      once: true,
    });
    xhr.upload.addEventListener("loadend", () =>
      window.removeEventListener(`phx:cancel-${entry.ref}`, abortUpload)
    );
    // END NEW LINES
    xhr.open("POST", url, true);
    xhr.send(formData);
  });
};
let liveSocket = new LiveSocket("/live", Socket, {
  uploaders: Uploaders,
  params: { _csrf_token: csrfToken },
});

The new lines 20-32 create a cancel event listener that will abort the xhr request if a phx:cancel-{uploadref} event is triggered. If the load completes (success or error), the xhr loadend event is triggered and will clean up the cancel listener.

Now, the only thing we have to do is push a cancel event from the LiveView.

  @impl true
  def handle_event("cancel-upload", %{"ref" => ref}, socket) do
    {:noreply,
     socket
     # This is the new line that cancels the xhr request
     |> push_event("cancel-#{ref}", %{})
     |> cancel_upload(:video, ref)}
  end

That's it. Now when the user cancels the upload, the browser will actually stop uploading the file.