LiveView Uploads multiple live_file_input

Hello. I have a problem with live uploading.
It works perfectly with a one file input in a form, but doesn’t with a couple file inputs.

I have live component where I enable uploading (in update(assign, socket) handler):

|> allow_upload(:file1, accept: ~w(.jpg .jpeg .gif .png), max_file_size: 2_000_000)
|> allow_upload(:file2, accept: ~w(.jpg .jpeg .gif .png), max_file_size: 2_000_000)

later in “save” handler I try to consume uploading for both of that files:
The code below is simplified as I created several help functions, but in the end it calls consume_uploaded_entries)

{:ok, file1} = consume_uploaded_entries(socket, :file1, fn ... end)
{:ok, file2} = consume_uploaded_entries(socket, :file2, fn ... end)

there is something like race conditions:

** (ArgumentError) cannot consume uploaded files when entries are still in progress
 (phoenix_live_view 0.15.0) lib/phoenix_live_view/upload.ex:212: Phoenix.LiveView.Upload.consume_uploaded_entries/3

and next error comes from GenServer because “save” was failed?
GenServer.call(#PID<0.31769.1>, {:phoenix, :register_entry_upload, %{channel_pid: #PID<0.31776.1>, cid: 3, entry_ref: "0", ref: "phx-Fk6-iIy54dK8vEXG"}}, 5000)

I tried to call consume_uploaded_entries one by one with a callback but it still doesn’t work… Do I need to drop entries manually or is it a limitation of current live view implementation?

Thanks

1 Like

I’ve got two uploaders in my applications and no issues. What does the EEx markup look like?

It’s something like that

<%= f = form_for @changeset, "#", [phx_target: @myself, phx_change: :validate, phx_submit: :save, multipart: true] %>
....

<%= inputs_for f, :settings, fn sf -> %>
  <%= inputs_for sf, :design, fn design_form -> %>
    <%= inputs_for design_form, :index_page, fn index_page -> %>
         <%= live_file_input @uploads.index_header_image, class: "custom-file-input" %>
    <% end>
   <%= inputs_for design_form, :detail_page, fn details_page -> %>
         <%= live_file_input @uploads.details_header_image, class: "custom-file-input" %>
    <% end>
  <% end>
<% end>

This seems fine to me :thinking: The entries should have been uploaded by the time the “save” event is triggered, according to the documentation:

When the end-user submits a form containing a live_file_input/2, the JavaScript client first uploads the file(s) before invoking the callback for the form’s phx-submit event.

I don’t have any good ideas unfortunately. Unless it is possible that the file system is somehow not writable due to some permission issues? On my Docker container files get uploaded to /tmp if I remember correctly. This is a bit of a wild guess but worth checking.

I wonder if you can get more debug info by implementing a progress function and printing the entry, which is available as an option in allow_upload: Phoenix.LiveView — Phoenix LiveView v0.20.2 .

@ole Actually, it’s worth re-writing your inputs_for functions to remove the anonymous function within, see this example:

LiveView does not support change tracking in anonymous functions or blocks, except for for/if/etc expressions. This might be getting in the way of setting up the uploaders. So your inputs_for will look similar to this:

<%= for design_form_inputs <- inputs_for(design_form, :index_page) do %>
  <%= live_file_input @uploads.index_header_image, class: "custom-file-input" %>
<% end %>

Maybe this would help? Note that you’ll need to convert all parent inputs_for as well.

I will check… actually it assigns correctly, I have helper that renders me entries for each input:

 <%= for entry <- @uploads[@name].entries do %>
    <div class="badge badge-<%= if entry.progress == 100, do: "success", else: "info" %> p-2 mt-1"><%= entry.client_name %> - <%= entry.progress %>%</div>
<% end %>

I see selected files. When I click it shows progress once one uploading is completed it fails with error. I tried to read a source code and see that it must check progress only for current input.
It fails immediately after completion on any live file input.
I attached a sample how does it looks like

I did a minimum reproducing case: no sub-components, only live view, no dependencies on any changeset or inherited form data. There is only a form, 2 live file inputs.
When I call consume_uploaded_entries for :file1 it works, when I call for both - error is still there:

> cannot consume uploaded files when entries are still in progress

I enabled socket’s debug logs and see there next picture (in case of one call of consume_uploaded_entries).

#Selected files on form:
phx-FlDrOCcb2UykHOJj update: ...  (file ref is phx-FlDrOFstQWyOAftH)
phx-FlDrOCcb2UykHOJj update: ... (file ref is phx-FlDrOFst1dyOAftn)

# Submitted form
phx-FlDrOCcb2UykHOJj upload: sending preflight request ...  (file ref is phx-FlDrOFstQWyOAftH)
phx-FlDrOCcb2UykHOJj upload: sending preflight request ... (file ref is phx-FlDrOFst1dyOAftn)
phx-FlDrOCcb2UykHOJj update ...  (file ref is phx-FlDrOFstQWyOAftH)
phx-FlDrOCcb2UykHOJj upload: got preflight response ...  (file ref is phx-FlDrOFstQWyOAftH, entries are already here for )

phx-FlDrOCcb2UykHOJj update ...  (2nd file ref is phx-FlDrOFst1dyOAftn)
phx-FlDrOCcb2UykHOJj upload: got preflight response ...  ( entries are already here for phx-FlDrOFst1dyOAftn)

phx-FlDrOCcb2UykHOJj update ... multiple quite similar calls for both files.

Html template:

<div class="col-md-12">
    <%= f = form_for :no_changeset, "#", [phx_change: :validate, phx_submit: :save, multipart: true] %>
    <%= submit "Save Design", phx_disable_with: "Saving...", class: "btn btn-primary" %>

    <div class="p-3">
        <div class="custom-file">
            <%= live_file_input @uploads.file1, class: "custom-file-input" %>
            <label class="custom-file-label" for="customFile">File1</label>
        </div>
        <%= for entry <- @uploads.file1.entries do %>
        <div class="badge badge-<%= if entry.progress == 100, do: "success", else: "info" %> p-2 mt-1"><%= entry.client_name %>
            - <%= entry.progress %>%
        </div>
        <% end %>
    </div>
    <div class="p-3">
        <div class="custom-file">
            <%= live_file_input @uploads.file2, class: "custom-file-input" %>
            <label class="custom-file-label" for="customFile">File2</label>
        </div>
        <%= for entry <- @uploads.file2.entries do %>
        <div class="badge badge-<%= if entry.progress == 100, do: "success", else: "info" %> p-2 mt-1"><%= entry.client_name %>
            - <%= entry.progress %>%
        </div>
        <% end %>
    </div>
</div>

Live View:

defmodule BackOffice.SettingsLive.Test do
  use BackOffice, :live_view

  def mount(params, session, socket) do
    {
      :ok,
      socket
      |> allow_upload(:file1, accept: ~w(.jpg .jpeg .gif .png), max_entries: 2, max_file_size: 2_000_000)
      |> allow_upload(:file2, accept: ~w(.jpg .jpeg .gif .png), max_entries: 2, max_file_size: 2_000_000)
    }
  end

  def render(assigns) do
    BackOffice.SettingsView.render("test.html", assigns)
  end

  def handle_params(_, _, socket) do
    {:noreply, socket}
  end

  def handle_event("validate", p, socket) do
    {:noreply, socket}
  end

  def handle_event("save", params, socket) do
    consume_uploaded_entries(
      socket,
      :file1,
      fn %{path: path}, entry ->
        :timer.sleep(300)
        path
      end
    )

    # Fails with this call on 2nd live_file_input
    # consume_uploaded_entries(
    #  socket,
    #  :file2,
    #  fn %{path: path}, entry ->
    #    :timer.sleep(300)
    #    path
    #  end
    #)

    {:noreply, socket}
  end

end

After updating data in socket entry disappears from the list of entries for the first file, the 2nd one shows as 100% progress, but is still there (if I have only one call of consume_uploaded_entries)

The problem is that view crashes before 2nd uploading completed with error I posted above. As a result it reloads view and there is one more js error in console.
Can’t figure out what I am doing wrong

Uncaught TypeError: Cannot read property 'closest' of null
    at e.value (phoenix_live_view.js?b42f:1)
    at e.value (phoenix_live_view.js?b42f:1)
    at e.value (phoenix_live_view.js?b42f:1)
    at e.value (phoenix_live_view.js?b42f:1)
    at Object.eval [as callback] (phoenix_live_view.js?b42f:1)
    at eval (phoenix.js?7b71:1)
    at Array.forEach (<anonymous>)
    at e.value (phoenix.js?7b71:1)
    at Object.eval [as callback] (phoenix.js?7b71:1)
    at e.value (phoenix.js?7b71:1)

Getting the same issue.

  • Two file inputs on page (multiple files allowed on each)
  • on form submit, it does wait for either input to complete upload but not both at the same time,
    looks like
{complete, partial} = uploaded_entries(socket,:file1)
# complete: [{.. progress: 100}]
# partial: []
{complete, partial} = uploaded_entries(socket,:file2)
# complete: []
# partial: [{.. progress: 0}]

...
** (ArgumentError) cannot consume uploaded files when entries are still in progress
    (phoenix_live_view) lib/phoenix_live_view/upload.ex:212: Phoenix.LiveView.Upload.consume_uploaded_entries/3

P.S. More information:

  • Live View 0.15.3 (latest)
  • It does wait for all images to be uploaded in one group (either one, whichever uploads first)
keys: [:input1, :input2]
:input completed: [
  %Phoenix.LiveView.UploadEntry{
    cancelled?: false,
    client_last_modified: nil,
    client_name: "foobar.jpg",
    client_size: 33345,
    client_type: "image/jpeg",
    done?: true,
    preflighted?: false,
    progress: 100,
    ref: "2",
    upload_config: :input1,
    upload_ref: "phx-Flb4CRApOHB6OKOB",
    uuid: "08ce6d7f-d19e-471a-99a2-a8e91b4a6686",
    valid?: true
  }
]
:input1 partial: []

:input2 completed: []
:input2 partial: [
  %Phoenix.LiveView.UploadEntry{
    cancelled?: false,
    client_last_modified: nil,
    client_name: "0sCtsmK.jpg",
    client_size: 682002,
    client_type: "image/jpeg",
    done?: false,
    preflighted?: false,
    progress: 22,
    ref: "3",
    upload_config: :input2,
    upload_ref: "phx-Flb4CRAp4Gh6OKOh",
    uuid: "32e05475-add0-4177-8e22-12de638a4f54",
    valid?: true
  }
]

P.S. Does LiveView have a helper for this, e.g. “wait for upload” ? Would manual re-check && :timer.sleep work? Probably not since socket is immutable
P.P.S. I think it’s javascript side - waiting code is there

yep, I am fighting with it right now and it’s quite weird.

I have the same issue, I only have 1 upload field with 1 max_entries.

To reproduce, this are the steps that make it fail systematically:

  • add two files into a max_entries: 1 upload field
  • remove one
  • quickly press submit
    :arrow_right: error, all uploads are not done yet

When I have two pending uploads (with an error as there’s one maximum), if I remove one to have a valid form and quickly press submit (less than 1 second after removing the extra upload), I see that the file is not uploaded (the progress bar remains at 0%) and the “save” fails as there are still pending uploads. There seems to be some kind of race condition between the upload field and the saving of the form. If I wait a couple of seconds before pressing save, the upload happens correctly before the save.
If on the “save” event, I wait for the upload to finish (I send a {:noreply, socket}), the form is never submitted and I have to press the submit button again (obviously, to trigger a new save event).

@achedeuzot I suppose it’s even easier to do the same by calling consume_uploading_ entries from “validate” event.
It looks like I found what was wrong with my code. When user selects a file and click upload first of all JS send files to the live-view where all references are alive and updates it’s progress. In “save” handler we tried to consume uploading entries: not sure if it’s correct explanation, but it’s something like transporting them from client side to our backend through channels in live view and it fails because not all of entries are completed (progress = 100).
Everything was changed when I added auto_upload: true option. I didn’t looks so deeply in JS code, I suppose it immediately starts uploading and probably sets some flags because it ignores form’s submit events before uploading completes and handle “save” after progress is 100% on all entries.

Hi,

For information, I’ve filled an issue regarding this behavior : Error with multiple live_file_input in one form · Issue #1347 · phoenixframework/phoenix_live_view · GitHub

Hi, thanks. could you try to add [auto_upload: true] on your test case?

Though we were able to make it work with auto_upload: true (thanks for the hint, by the way :slight_smile: ), I suspect that it just gives more time for uploads to finish, hence limiting the issue (to be clear, that is a wild guest ^^).

I didn’t check source code yet, but I feel like time for uploads doesn’t matter. You can try one very small file, a very big, and you will see - there is no race conditions… I suppose on JS there is some callbacks/promises that works correctly with auto_upload: true, but when you trigger it from a backend using consume_file_uploads → it doesn’t happen and separated JS processes (may be not correct naming) handle it and once one completes uploading it triggers form submition. With auto_upload it waits until phoenix server will complete all uploads.

You’re right. Using auto_upload: true is a working workaround, thanks! I’ve added a comment to the issue to help people finding this solution.

Hello, everyone! I believe we have a fix for this :slight_smile:

I created a demo for a quick test - GitHub - mcrumm/live_upload_example at mc-multiple-inputs

Thanks for the reports @ole and @gorghoa !

4 Likes

Thanks to you @mcrumm (and to my coworker @Gnookiie which has pointed this issue to my attention first :wink: ) ! Glad to see our feedback being valuated and taken good care of! :slight_smile:

Plus, a fix that remove code instead of adding complexity is always a joy to see!

Thanks @mcrumm

Hi!

I still get errors with uploading more than one file at once with auto_upload: true
I updated to live_view 0.15.5

regards

[error] GenServer #PID<0.3985.0> terminating
** (ArgumentError) cannot consume uploaded files when entries are still in progress
    (phoenix_live_view 0.15.5) lib/phoenix_live_view/upload.ex:218: Phoenix.LiveView.Upload.consume_uploaded_entries/3