phoenix_live_view icon indicating copy to clipboard operation
phoenix_live_view copied to clipboard

Long running uploads via external uploaders cause problems when reusing websocket connections

Open RobinBoers opened this issue 1 year ago • 2 comments

Environment

  • Elixir version: 1.16.2
  • Phoenix version: 1.7.14
  • Phoenix LiveView version: 1.0.0-rc.6
  • Operating system: MacOS 14.5 (arm64)
  • Browsers you attempted to reproduce this bug on: Firefox 129.0b9, Chrome 127.0.6533.74
  • Does the problem persist after removing "assets/node_modules" and trying again? Yes

Actual behavior

  • Start a long-running upload.
  • Remount the current LiveView during upload (clicking "same, but different" in my example).

This should create a new LiveView process that reuses the existing websocket connection. The new LiveView again sets up uploads.

However, on the frontend, the JavaScript happily keeps uploading and sending messages to the over the existing websocket connection, to the new LiveView process. This new LiveView process doesn't have the old upload state, thus crashes like this:

** (KeyError) key "phx-F-e1tog74Q2dEAgB" not found in: %{"phx-F-e1wlp0SMNl9giB" => :videos}
    :erlang.map_get("phx-F-e1tog74Q2dEAgB", %{"phx-F-e1wlp0SMNl9giB" => :videos})
    (phoenix_live_view 1.0.0-rc.6) lib/phoenix_live_view/upload.ex:196: Phoenix.LiveView.Upload.get_upload_by_ref!/2
    (phoenix_live_view 1.0.0-rc.6) lib/phoenix_live_view/upload.ex:126: Phoenix.LiveView.Upload.update_progress/4
    (phoenix_live_view 1.0.0-rc.6) lib/phoenix_live_view/channel.ex:176: anonymous fn/4 in Phoenix.LiveView.Channel.handle_info/2
    (phoenix_live_view 1.0.0-rc.6) lib/phoenix_live_view/channel.ex:1419: Phoenix.LiveView.Channel.write_socket/4
    (phoenix_live_view 1.0.0-rc.6) lib/phoenix_live_view/channel.ex:174: Phoenix.LiveView.Channel.handle_info/2

A similar thing happens when navigating to another LiveView:

  • Start a long-running upload.
  • Navigate away (clicking "another page" in my example).

In my example, the new LiveView process didn't call allow_uploads, and thus doesn't have an uploading state, raising this error when the JavaScript happily reports upload progress:

** (ArgumentError) no uploads have been allowed on LiveView named ReproductionWeb.OtherLive
    (phoenix_live_view 1.0.0-rc.6) lib/phoenix_live_view/upload.ex:195: Phoenix.LiveView.Upload.get_upload_by_ref!/2
    (phoenix_live_view 1.0.0-rc.6) lib/phoenix_live_view/upload.ex:126: Phoenix.LiveView.Upload.update_progress/4
    (phoenix_live_view 1.0.0-rc.6) lib/phoenix_live_view/channel.ex:176: anonymous fn/4 in Phoenix.LiveView.Channel.handle_info/2
    (phoenix_live_view 1.0.0-rc.6) lib/phoenix_live_view/channel.ex:1419: Phoenix.LiveView.Channel.write_socket/4
    (phoenix_live_view 1.0.0-rc.6) lib/phoenix_live_view/channel.ex:174: Phoenix.LiveView.Channel.handle_info/2

Expected behavior

I would expect running uploads to either:

RobinBoers avatar Aug 01 '24 21:08 RobinBoers

Update: it only happens with JS-based external uploaders. Phoenix.LiveView.UploadWriters are terminated correctly when navigating. (See RobinBoers/lv-long-running-uploads#upload-writers).

I think this issue is related to #3287. Seems like the proposed solution there (adding a cancel callback to the JS uploader), would also work in our case.

RobinBoers avatar Aug 02 '24 07:08 RobinBoers

hacked something just to appease the errors:

window.addEventListener("phx:page-loading-start", e => {
  let el = document.getElementById("confirm-unsaved-upload")
  if (el && el.hasAttribute("data-uploading") && el.getAttribute("data-uploading") === "true") {
    if (confirm("Changes you made may not be saved")) {
      activeUploads.forEach(entry => {
        console.log(entry)
        entry.abort()
      })
    } else {
      // window.location.assign = e.state?.previousUrl || '/'
      // window.location.replace(window.location.href)
      history.pushState(null, null, window.location.href) // This pushes the original URL back to the history, effectively "cancelling" the popstate.
    }
  }
  topbar.show(300)
})

Though you have to add beforeunload if you want to ask the user when doing page reloads.

@RobinBoers yeah and #3287. Is there a workaround for this related ?websocket? issues? seems like doing lv-side of things is great until you're making external calls

jaeyson avatar Aug 27 '25 13:08 jaeyson