LiveView callbacks firing on websocket heartbeat

Possibly related to Liveview form keeps on refreshing every 15s , but my problem is slightly different IMO.

I have a callback (“status”) that should only be firing when the value is changed in the select box on the form. Unfortunately, it is firing when the form is idle and the liveview heartbeat happens. This problem is compounded by the fact that it sends out a notification and spams the mobile user’s app.

I’ve provided the select and callback code blocks below:

<td class="td">
  <div class="flex-container <%= get_status_class(row) %>">
    <div class="dropdown-wrapper">
    <%= f = form_for :f, "#", [class: "form form-group", phx_change: :status] %>
      <%= hidden_input f, :sub_id, value: row.subscription_id %>
      <%= hidden_input f, :user_id, value: row.user_id %>
      <%= hidden_input f, :dep_id, value: row.dependent_id %>
      <%= case row do %>
        <% %{connection: :inactive} -> %>
          <%= select f, :status, @paused_statuses, selected: row.connection, disabled: true, class: "flex-item MR15 dropdown js-status-dropdown", id: "status-#{row.subscription_id}-#{row.id}" %>
        <% _ -> %>
          <%= select f, :status, @connection_statuses, selected: row.connection, class: "flex-item MR15 dropdown js-status-dropdown", id: "status-#{row.subscription_id}-#{row.id}" %>
      <% end %>
    </form>
    </div>
  </div>
</td>
@impl true
  def handle_event(
        "status",
        %{"f" => %{"status" => status, "sub_id" => id, "user_id" => user_id, "dep_id" => ""}},
        socket
      ) do
    _ =
      Logger.info(
        "#{__MODULE__}: event: status: #{inspect(status)}. sub_id: #{inspect(id)}, user_id: #{
          inspect(user_id)
        }"
      )
    status = String.to_atom(status)
    case Connection.update_status(id, status) do
      :error ->
        _ =
          Logger.info(
            "#{__MODULE__}: event: Failed to update: #{inspect(status)}. sub_id: #{inspect(id)}, user_id: #{
              inspect(user_id)
            }"
          )
        {:noreply, socket}
      :ok ->
        # Update the assign data with the new status value to update the view
        {:noreply, assign(socket, data: update_view_status(socket.assigns.data, user_id, status))}
    end
  end

Any thoughts?

Unrelated to the issue itself, but this is dangerous. A user can use this to exhaust your atom table, leading to crash of the VM. You should manually convert allowed values to atoms or use String.to_existing_atom/1 that doesn’t allow creation of new atoms.

3 Likes

Good catch … I forgot about String.to_existing_atom because we stopped using String.to_atom for input values some time back. So, you are saying that somebody could send values different than the three in the dropdown and crash our VM?

Yes, the atom table has a fixed upper size limit. If you try to create more atoms, the VM crashes. For example:

iex(1)> for i <- 1..1000000000, do: String.to_atom("test-#{i}")
no more index entries in atom_tab (max=1048576)

Crash dump is being written to: erl_crash.dump...done
2 Likes

Other strange behavior on the original issue, is that the callbacks are being fired, but having no effect on the page when I return to it. For example, on one page the heartbeat is firing and calling the validate callback. I have added logging so I know the validate is being called, but there is no data on the form and there is no visual indication that the form did anything.

This one was only discoverable by logging because it doesn’t have the nasty side effect of spamming our mobile users, but strange nonetheless.

I lost the link to where I found this, but setting phx_auto_recover: “ignore” in the form attributes appears to have fixed our issue.