handle_info with lifecycle callbacks

I stumbled across a strange behavior which I could not reproduce debugging my code. We use Token based authentication in our app and implemented a lifecycle callback which allows us to perform an instant logout, if the user ends his session on the identity provider. We pass this function as an option for the live_session macro in the router:

def on_mount(:default, _params, session, socket) do
    if connected?(socket), do: Accounts.subscribe()

    current_user = Accounts.get_user(session["current_user_id"])
    locale = if current_user, do: current_user.locale, else: session["locale"]
    Gettext.put_locale(Web.Gettext, locale)

    socket =
      socket
      |> assign_new(:locale, fn -> locale end)
      |> assign_new(:current_user, fn -> current_user end)
      |> attach_hook(:sso_logout_hook, :handle_info, fn
        {Core.Accounts, "user-logged-out", payload}, socket ->
          if socket.assigns.current_user.origin_id == payload["sub"] do
            {:halt, redirect(socket, to: Routes.session_path(socket, :logout))}
          else
            {:cont, socket}
          end

        _params, socket ->
          {:cont, socket}
      end)

    {:cont, socket}
  end

It works as expected, but not when another module is implementing other :handle_info callbacks. Does anyone know what this problem might be?

What does the “other module” hook look like? Does the other handle_info hook have a catch-all with {:cont, socket} that forwards the message to downstream hooks?

Oh, i’m sorry, with other module I was referring to another LiveView (in this case called MachineLive.Index). There are no additional hooks only the already described one. For example we have implemented multiple handle_info callbacks in this LiveView. One of them is the following:

@impl true
  def handle_info({Machines, "machine-changed", payload}, socket) do
    socket =
      if payload[:account_id] == socket.assigns.current_account.id do
        fetch_and_assign_machines(
          socket,
          socket.assigns.current_account.id,
          socket.assigns.params
        )
      else
        socket
      end

    {:noreply, socket}
  end

In this LiveView we receive the logout event via PubSub, but it seems that the previously assigned sso_logout_hook hook is not available at that time. We get the following exception:

FunctionClauseError: no function clause matching in Web.Machines.MachineLive.Index.handle_info/2
  File "lib/web/live/machines/machine_live/index.ex", line 97, in Web.Machines.MachineLive.Index.handle_info/2
  File "lib/phoenix_live_view/channel.ex", line 276, in Phoenix.LiveView.Channel.handle_info/2
  File "gen_server.erl", line 1123, in :gen_server.try_dispatch/4
  File "gen_server.erl", line 1200, in :gen_server.handle_msg/6
  File "proc_lib.erl", line 250, in :proc_lib.wake_up/3

I was thinking that I can make the ´sso_logout_hook´ for all live views globally accessible when mounted via attach_hook. Maybe I got that wrong :thinking:

Can you share the full arguments in the stacktrace? Are you 100% positive that you:

  1. are mounting the hook for the Index LV
  2. In your hook, {Core.Accounts, "user-logged-out", payload} is the actual payload you broadcast for disconnect?

The hook is mounted within the router. This seems to be okay:

 live_session :authenticated, on_mount: Web.InitAssigns do
    scope "/", Web do
      pipe_through [:browser, :ensure_authenticated]

      scope "/machines", Machines, as: :machine do
        live "/machines", MachineLive.Index, :index
      end
end

The payload is the expected payload (arg0):

and as a complete sanity check, you’ve added logging in your hook handle_info to verify that this condition is matching or no?

if socket.assigns.current_user.origin_id == payload["sub"] do

It’s possible your hook is being called and continuing the message downstream in the else clause. Have you ruled that out?

I checked the if-clause and the condition is matching. Do I need to halt the connection and detach the hook when the else clause is interpreted? There is an example within the documentation about assigning hooks to the handle_event stage. I’ve tried to follow that example.

Is the else clause supposed to disconnect the user or swallow the event? {:cont, msg} forwards the message downstream, including to the LV.

Sounds like you want {:halt, socket}