Sporadic error due to race condition on file access in live_file_input?

Hi, am seeing a weird behavior -
I get a crash about 1/3rd of the time … am basically trying to read the file in the handle_progress callback with auto_upload: true. This is straight from the examples in the docs …
It feels like a race condition between the file being available for file I/O and the progress event being issued by phoenix (perhaps phoenix is faster than windows :wink: ).

The upload_ref lines up between the validate callback and handle_progress callback so, nothing obvious for me to go with …

Is there some trick I am missing (a delay or something that needs to be hacked in ?)
Has anyone else encountered this ?

Code below :


  @impl true
  def mount(socket) do
    {:ok, socket
          |> assign(:uploaded_files, [])
          |> allow_upload(:json_file, accept: ~w(.json), max_entries: 1, progress: &handle_progress/3, auto_upload: true, max_file_size: 10_000)}
  end


  defp handle_progress(:json_file, entry, socket) do
    if entry.done? do
      uploaded_file =
        consume_uploaded_entry(socket, entry, fn %{} = meta ->
          IO.puts("*** consume_file/handle_progress - uploaded [#{inspect(meta)}]")
          {:ok, meta}
        end)

      config =
        uploaded_file.path
        |> File.read!
        |> Jason.decode!()

      IO.inspect(config, label: "**** config")

      {:noreply, put_flash(socket, :info, "file #{uploaded_file.path} uploaded")}
    else
      {:noreply, socket}
    end
  end

Crash dump :

*** consume_file/handle_progress - uploaded [%{path: "c:/Users/milan/AppData/Local/Temp/plug-1663/live_view_upload-1663999474-28197883582029-1"}]

[02:04:34.076][error][domain=otp mfa=:gen_server.error_info/8 line=1399 ]🌼GenServer #PID<0.9403.0> terminating
**** (File.Error) could not read file "c:/Users/milan/AppData/Local/Temp/plug-1663/live_view_upload-1663999474-28197883582029-1": no such file or directory**
**    (elixir 1.14.0) lib/file.ex:358: File.read!/1**
    (meta 2.266.539) lib/meta_web/live/admin/integration_live/form_component.ex:27: MetaWeb.Admin.IntegrationLive.FormComponent.handle_progress/3
    (phoenix_live_view 0.17.11) lib/phoenix_live_view/channel.ex:151: anonymous fn/4 in Phoenix.LiveView.Channel.handle_info/2
    (phoenix_live_view 0.17.11) lib/phoenix_live_view/diff.ex:206: Phoenix.LiveView.Diff.write_component/4
    (phoenix_live_view 0.17.11) lib/phoenix_live_view/channel.ex:1207: Phoenix.LiveView.Channel.write_socket/4
    (phoenix_live_view 0.17.11) lib/phoenix_live_view/channel.ex:144: Phoenix.LiveView.Channel.handle_info/2
    (stdlib 4.0.1) gen_server.erl:1120: :gen_server.try_dispatch/4
    (stdlib 4.0.1) gen_server.erl:1197: :gen_server.handle_msg/6
    (stdlib 4.0.1) proc_lib.erl:250: :proc_lib.wake_up/3

You should be surprised that what you’re doing is working sometimes, not that it doesn’t work all the times :slight_smile:

As the docs say:

Once entries are consumed, they are removed from the upload.

So you should do whatever you need to do with the file in the consume_uploaded_entry callback. Don’t rely on the original copy of the file being still there when consume_uploaded_entry exits. If you notice, all examples in the docs copy the file to a different location.

2 Likes

My god ! I have to stop coding at 2 am :blush:

In this scenario, just returning the same tmp file name (reusing it) is not supported ?

My use case is simple … am trying to preview the file (it isn’t an image) and process its contents. I don’t want to make a copy of the file on the server beyond whatever phoenix is doing internally (temp store).

Ok. Finally lightbulb went on ! Thank you @trisolaran

Much much prettier … a lot better when you understand the thinking of how/why the api was designed/named.

This works nicely for my use case … am assuming the file is safe within the consume_uploaded_entry function.

  defp handle_progress(:json_file, entry, socket) do
    if entry.done? do
      config =
        consume_uploaded_entry(socket, entry, fn %{} = meta ->
          IO.puts("*** consume_file/handle_progress - uploaded [#{inspect(meta)}]")
          contents =
            meta.path
            |> File.read!
            |> Jason.decode!()
            {:ok, contents}
        end)

      IO.inspect(config, label: "**** config")

      {:noreply, put_flash(socket, :info, "config uploaded")}
    else
      {:noreply, socket}
    end
  end
1 Like