Can't attach hook to :mount stage via on_mount/4 callback

Hi everyone!

I trying to create a Phoenix LiveView dependency to authorize the lifecycle stages with attach_hook/4 via on_mount/4 callback.
I need to attach hook to :mount stage and it possible with the on_mount/4 callback as described here. I don’t know why I getting the following error:

# ArgumentError at GET /some/route

Exception:

    ** (ArgumentError) invalid lifecycle event provided to attach_hook.
    
    Expected one of: :handle_event | :handle_info | :handle_params
    
    Got: :mount
    
        (phoenix_live_view 0.20.1) lib/phoenix_live_view/lifecycle.ex:61: Phoenix.LiveView.Lifecycle.attach_hook/4
        (elixir 1.15.7) lib/enum.ex:2510: Enum."-reduce/3-lists^foldl/2-0-"/3
        (dronteh 0.1.0) lib/my_dependency_web/on_mount.ex:6: MyDependency.OnMount.on_mount/4
        (phoenix_live_view 0.20.1) lib/phoenix_live_view/lifecycle.ex:149: anonymous fn/4 in Phoenix.LiveView.Lifecycle.mount/3
        (phoenix_live_view 0.20.1) lib/phoenix_live_view/lifecycle.ex:212: Phoenix.LiveView.Lifecycle.reduce_socket/3
        (phoenix_live_view 0.20.1) lib/phoenix_live_view/utils.ex:392: anonymous fn/6 in Phoenix.LiveView.Utils.maybe_call_live_view_mount!/5
        (telemetry 1.2.1) /app/deps/telemetry/src/telemetry.erl:321: :telemetry.span/3
        (phoenix_live_view 0.20.1) lib/phoenix_live_view/static.ex:277: Phoenix.LiveView.Static.call_mount_and_handle_params!/5
        (phoenix_live_view 0.20.1) lib/phoenix_live_view/static.ex:118: Phoenix.LiveView.Static.render/3
        (phoenix_live_view 0.20.1) lib/phoenix_live_view/controller.ex:39: Phoenix.LiveView.Controller.live_render/3
        (phoenix 1.7.9) lib/phoenix/router.ex:432: Phoenix.Router.__call__/5
        (dronteh 0.1.0) lib/dronteh_web/endpoint.ex:1: DrontehWeb.Endpoint.plug_builder_call/2
        (dronteh 0.1.0) deps/plug/lib/plug/debugger.ex:136: DrontehWeb.Endpoint."call (overridable 3)"/2
        (dronteh 0.1.0) lib/dronteh_web/endpoint.ex:1: DrontehWeb.Endpoint.call/2
        (phoenix 1.7.9) lib/phoenix/endpoint/sync_code_reload_plug.ex:22: Phoenix.Endpoint.SyncCodeReloadPlug.do_call/4
        (plug_cowboy 2.6.1) lib/plug/cowboy/handler.ex:11: Plug.Cowboy.Handler.init/2
        (cowboy 2.10.0) /app/deps/cowboy/src/cowboy_handler.erl:37: :cowboy_handler.execute/2
        (cowboy 2.10.0) /app/deps/cowboy/src/cowboy_stream_h.erl:306: :cowboy_stream_h.execute/3
        (cowboy 2.10.0) /app/deps/cowboy/src/cowboy_stream_h.erl:295: :cowboy_stream_h.request_process/3
        (stdlib 5.1.1) proc_lib.erl:241: :proc_lib.init_p_do_apply/3
    

Code:

`lib/phoenix_live_view/lifecycle.ex`

    56         hooks ++ [hook]
    57       end)
    58     end
    59   
    60     def attach_hook(%Socket{}, _id, stage, _fun) do
    61>      raise ArgumentError, """
    62       invalid lifecycle event provided to attach_hook.
    63   
    64       Expected one of: :handle_event | :handle_info | :handle_params
    65   
    66       Got: #{inspect(stage)}
    
`lib/enum.ex`

    2505     operation cannot be expressed by any of the functions in the `Enum`
    2506     module, developers will most likely resort to `reduce/3`.
    2507     """
    2508     @spec reduce(t, acc, (element, acc -> acc)) :: acc
    2509     def reduce(enumerable, acc, fun) when is_list(enumerable) do
    2510>      :lists.foldl(fun, acc, enumerable)
    2511     end
    2512   
    2513     def reduce(first..last//step, acc, fun) do
    2514       reduce_range(first, last, step, acc, fun)
    2515     end
    
`lib/my_dependency_web/on_mount.ex`

    1     defmodule MyDependency do
    2       import Phoenix.LiveView
    3
    4       def on_mount(:default, _params, _session, socket) do
    5         # some code...
    6         socket = attach_hook(socket, "protect_mount", :mount, fn _params, _session, socket) ->
    7           # some code...
    8
    9           {:cont, socket} or {:halt socket} # with redirect
    10        end)
    11
    
`lib/phoenix_live_view/lifecycle.ex`

    144     end
    145   
    146     @doc false
    147     def mount(params, session, %Socket{private: %{@lifecycle => lifecycle}} = socket) do
    148       reduce_socket(lifecycle.mount, socket, fn %{id: {_mod, arg}} = hook, acc ->
    149>        case hook.function.(arg, params, session, acc) do
    150           {:halt, %Socket{redirected: nil}} ->
    151             raise_halt_without_redirect!(hook)
    152   
    153           {:cont, %Socket{redirected: to}} when not is_nil(to) ->
    154             raise_continue_with_redirect!(hook)
    
`lib/phoenix_live_view/lifecycle.ex`

    207   
    208       new_socket
    209     end
    210   
    211     defp reduce_socket([hook | hooks], acc, function) do
    212>      case function.(hook, acc) do
    213         {:cont, %Socket{} = socket} -> reduce_socket(hooks, socket, function)
    214         {:halt, %Socket{} = socket} -> {:halt, socket}
    215         other -> bad_lifecycle_response!(other, hook)
    216       end
    217     end
    
`lib/phoenix_live_view/utils.ex`

    387         :telemetry.span(
    388           [:phoenix, :live_view, :mount],
    389           %{socket: socket, params: params, session: session, uri: uri},
    390           fn ->
    391             socket =
    392>              case Lifecycle.mount(params, session, socket) do
    393                 {:cont, %Socket{} = socket} when exported? ->
    394                   view.mount(params, session, socket)
    395   
    396                 {_, %Socket{} = socket} ->
    397                   {:ok, socket}
    
`/app/deps/telemetry/src/telemetry.erl`

    316           EventPrefix ++ [start],
    317           #{monotonic_time => StartTime, system_time => erlang:system_time()},
    318           merge_ctx(StartMetadata, DefaultCtx)
    319       ),
    320   
    321>      try {_, #{}} = SpanFunction() of
    322         {Result, StopMetadata} ->
    323             StopTime = erlang:monotonic_time(),
    324             execute(
    325                 EventPrefix ++ [stop],
    326                 #{duration => StopTime - StartTime, monotonic_time => StopTime},
    
`lib/phoenix_live_view/static.ex`

    272   
    273     defp call_mount_and_handle_params!(socket, view, session, params, uri) do
    274       mount_params = if socket.router, do: params, else: :not_mounted_at_router
    275   
    276       socket
    277>      |> Utils.maybe_call_live_view_mount!(view, mount_params, session, uri)
    278       |> mount_handle_params(view, params, uri)
    279       |> case do
    280         {:noreply, %Socket{redirected: {:live, _, _}} = socket} ->
    281           {:stop, socket}
    282   
    
`lib/phoenix_live_view/static.ex`

    113           action,
    114           flash,
    115           host_uri
    116         )
    117   
    118>      case call_mount_and_handle_params!(socket, view, mount_session, conn.params, request_url) do
    119         {:ok, socket} ->
    120           data_attrs = [
    121             phx_session: sign_root_session(socket, router, view, to_sign_session, live_session),
    122             phx_static: sign_static_token(socket)
    123           ]
    
`lib/phoenix_live_view/controller.ex`

    34           end
    35         end
    36   
    37     """
    38     def live_render(%Plug.Conn{} = conn, view, opts \\ []) do
    39>      case LiveView.Static.render(conn, view, opts) do
    40         {:ok, content, socket_assigns} ->
    41           conn
    42           |> Phoenix.Controller.put_view(LiveView.Static)
    43           |> Phoenix.Controller.render(
    44             "template.html",
    
`lib/phoenix/router.ex`

    427           :telemetry.execute([:phoenix, :router_dispatch, :stop], measurements, metadata)
    428           halted_conn
    429   
    430         %Plug.Conn{} = piped_conn ->
    431           try do
    432>            plug.call(piped_conn, plug.init(opts))
    433           else
    434             conn ->
    435               measurements = %{duration: System.monotonic_time() - start}
    436               metadata = %{metadata | conn: conn}
    437               :telemetry.execute([:phoenix, :router_dispatch, :stop], measurements, metadata)
    
`lib/dronteh_web/endpoint.ex`

    1>  defmodule DrontehWeb.Endpoint do
    2     use Phoenix.Endpoint, otp_app: :dronteh
    3   
    4     # The session will be stored in the cookie and signed,
    5     # this means its contents can be read but not tampered with.
    6     # Set :encryption_salt if you would also like to encrypt it.
    
`deps/plug/lib/plug/debugger.ex`

    131             case conn do
    132               %Plug.Conn{path_info: ["__plug__", "debugger", "action"], method: "POST"} ->
    133                 Plug.Debugger.run_action(conn)
    134   
    135               %Plug.Conn{} ->
    136>                super(conn, opts)
    137             end
    138           rescue
    139             e in Plug.Conn.WrapperError ->
    140               %{conn: conn, kind: kind, reason: reason, stack: stack} = e
    141               Plug.Debugger.__catch__(conn, kind, reason, stack, @plug_debugger)
    
`lib/dronteh_web/endpoint.ex`

    1>  defmodule DrontehWeb.Endpoint do
    2     use Phoenix.Endpoint, otp_app: :dronteh
    3   
    4     # The session will be stored in the cookie and signed,
    5     # this means its contents can be read but not tampered with.
    6     # Set :encryption_salt if you would also like to encrypt it.
    
`lib/phoenix/endpoint/sync_code_reload_plug.ex`

    17   
    18     def call(conn, {endpoint, opts}), do: do_call(conn, endpoint, opts, true)
    19   
    20     defp do_call(conn, endpoint, opts, retry?) do
    21       try do
    22>        endpoint.call(conn, opts)
    23       rescue
    24         exception in [UndefinedFunctionError] ->
    25           case exception do
    26             %UndefinedFunctionError{module: ^endpoint} when retry? ->
    27               # Sync with the code reloader and retry once
    
`lib/plug/cowboy/handler.ex`

    6     def init(req, {plug, opts}) do
    7       conn = @connection.conn(req)
    8   
    9       try do
    10         conn
    11>        |> plug.call(opts)
    12         |> maybe_send(plug)
    13         |> case do
    14           %Plug.Conn{adapter: {@connection, %{upgrade: {:websocket, websocket_args}} = req}} = conn ->
    15             {handler, state, cowboy_opts} = websocket_args
    16             {__MODULE__, copy_resp_headers(conn, req), {handler, state}, cowboy_opts}
    
`/app/deps/cowboy/src/cowboy_handler.erl`

    32   -optional_callbacks([terminate/3]).
    33   
    34   -spec execute(Req, Env) -> {ok, Req, Env}
    35   	when Req::cowboy_req:req(), Env::cowboy_middleware:env().
    36   execute(Req, Env=#{handler := Handler, handler_opts := HandlerOpts}) ->
    37>  	try Handler:init(Req, HandlerOpts) of
    38   		{ok, Req2, State} ->
    39   			Result = terminate(normal, Req2, State, Handler),
    40   			{ok, Req2, Env#{result => Result}};
    41   		{Mod, Req2, State} ->
    42   			Mod:upgrade(Req2, Env, Handler, State);
    
`/app/deps/cowboy/src/cowboy_stream_h.erl`

    301   	end.
    302   
    303   execute(_, _, []) ->
    304   	ok;
    305   execute(Req, Env, [Middleware|Tail]) ->
    306>  	case Middleware:execute(Req, Env) of
    307   		{ok, Req2, Env2} ->
    308   			execute(Req2, Env2, Tail);
    309   		{suspend, Module, Function, Args} ->
    310   			proc_lib:hibernate(?MODULE, resume, [Env, Tail, Module, Function, Args]);
    311   		{stop, _Req2} ->
    
`/app/deps/cowboy/src/cowboy_stream_h.erl`

    290   %% to simplify the debugging of errors. The proc_lib library
    291   %% already adds the stacktrace to other types of exceptions.
    292   -spec request_process(cowboy_req:req(), cowboy_middleware:env(), [module()]) -> ok.
    293   request_process(Req, Env, Middlewares) ->
    294   	try
    295>  		execute(Req, Env, Middlewares)
    296   	catch
    297   		exit:Reason={shutdown, _}:Stacktrace ->
    298   			erlang:raise(exit, Reason, Stacktrace);
    299   		exit:Reason:Stacktrace when Reason =/= normal, Reason =/= shutdown ->
    300   			erlang:raise(exit, {Reason, Stacktrace}, Stacktrace)
    
`proc_lib.erl`

    No code available.


## Connection details

### Params

    %{}

### Request info

  * URI: http://localhost:4000/some/route
  * Query string: 

### Headers
  
  * accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8
  * accept-encoding: gzip, deflate, br
  * accept-language: en-US,en;q=0.9,hu;q=0.8,sr;q=0.7
  * cache-control: max-age=0
  * connection: keep-alive
  * cookie: _dronteh_key=SFMyNTY.g3QAAAADbQAAAAtfY3NyZl90b2tlbm0AAAAYNEZzX2RLRWpkOXBqbFE1TGJvR0lqcVI2bQAAAA5saXZlX3NvY2tldF9pZG0AAAA7dXNlcnNfc2Vzc2lvbnM6M2E4VW9vN1hBRm51S3hZTWF2S2cyN0RmNHFVZlltQk1obDBZczBxTXBSRT1tAAAACnVzZXJfdG9rZW5tAAAAIN2vFKKO1wBZ7isWDGryoNuw3-KlH2JgTIZdGLNKjKUR.lBUv7RmSNBZqPQ9UZHJ2Lb8_KokcFoukItDpXDw-aGw
  * host: localhost:4000
  * referer: http://localhost:4000/some/route
  * sec-ch-ua: "Brave";v="119", "Chromium";v="119", "Not?A_Brand";v="24"
  * sec-ch-ua-mobile: ?0
  * sec-ch-ua-platform: "Windows"
  * sec-fetch-dest: document
  * sec-fetch-mode: navigate
  * sec-fetch-site: same-origin
  * sec-gpc: 1
  * upgrade-insecure-requests: 1
  * user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36

### Session

    %{"_csrf_token" => "4Fs_dKEjd9pjlQ5LboGIjqR6", "live_socket_id" => "users_sessions:3a8Uoo7XAFnuKxYMavKg27Df4qUfYmBMhl0Ys0qMpRE=", "user_token" => <<221, 175, 20, 162, 142, 215, 0, 89, 238, 43, 22, 12, 106, 242, 160, 219, 176, 223, 226, 165, 31, 98, 96, 76, 134, 93, 24, 179, 74, 140, 165, 17>>}

My code is the following:

defmodule  MyAppWeb.SomeLive do
  use MyAppWeb, :live_view

  on_mount MyDependency

  def mount(_params, _session, socket) do
    # some code...

    {:ok, socket}
  end

  # other code...
end
defmodule MyDependency do
  import Phoenix.LiveView

  def on_mount(:default, _params, _session, socket) do
    # some code...
    socket = attach_hook(socket, "protect_mount", :mount, fn _params, _session, socket) ->
      # some code...

      {:cont, socket} or {:halt socket} # with redirect
    end)

    {:cont, socket}
  end
end

Of course my code has more logic but this is the main idea. I got that error in other projects too.
Can somebody tell me what I doing wrong? :confused:

Versions I use:

  • elixir: ~> 1.15.7
  • phoenix: ~> 1.7.9
  • phoenix_live_view: ~> 0.20.1

Welcome!

No need for this attach_hook since on_mount is already a hook on mount which is why attach_hook doesn’t accept :mount as a valid lifecycle event.

Instead, give something like this a try:

def on_mount(:protect_mount, _params, _session, socket) do
  # some code...
  {:cont, socket}
end
1 Like

Thanks for the reply! :grin:
Hmm yeah this is makes sense. It seems like I misunderstood the docs.
My goal is the user can write pattern matching scenarios to those methods which can be attached hook to and pattern match such as the :mount stage as I already did with :handle_params and the other lifecycle methods via the attach_hook/4. But I think I can do that what you suggested but with some macro. :thinking:

1 Like