Hi! I’m working on changing phx.gen.auth to work with JSON only. It’s going stellar, but I’ve faced an issue and I was wondering if someone could tell me what’s going on under the hood.
I’ve modified the :api plug to use sessions, and that’s going fine. I intend to use session tokens to authenticate. All views were changed to respond with JSON (using controller_json.ex files). My plug looks like this:
pipeline :api do
plug :accepts, ["json"]
# Use the same plugs for authentication as the browser pipeline.
plug :fetch_session
plug :put_secure_browser_headers
plug :fetch_current_user
end
Note the absence of any flash plugs–being an API, this isn’t needed here.
Now, while transitioning a controller to return JSON, everything was going fine until this code:
def create(conn, %{"user" => %{"email" => email}}) do
if user = Accounts.get_user_by_email(email) do
Accounts.deliver_user_confirmation_instructions(
user,
&url(~p"/users/confirm/#{&1}")
)
end
render(conn, :create)
end
When calling this controller, the request went through, but I ended up with this error:
Server: localhost:4000 (http)
Request: POST /accounts/confirm/
** (exit) an exception was raised:
** (ArgumentError) flash not fetched, call fetch_flash/2
(phoenix 1.7.10) lib/phoenix/controller.ex:1686: Phoenix.Controller.put_flash/3
(timeline 0.1.0) lib/timeline_web/controllers/user_confirmation_controller.ex:19: TimelineWeb.UserConfirmationController.create/2
(timeline 0.1.0) lib/timeline_web/controllers/user_confirmation_controller.ex:1: TimelineWeb.UserConfirmationController.action/2
(timeline 0.1.0) lib/timeline_web/controllers/user_confirmation_controller.ex:1: TimelineWeb.UserConfirmationController.phoenix_controller_pipeline/2
(phoenix 1.7.10) lib/phoenix/router.ex:432: Phoenix.Router.__call__/5
(timeline 0.1.0) lib/timeline_web/endpoint.ex:1: TimelineWeb.Endpoint.plug_builder_call/2
(timeline 0.1.0) /Users/sergio/Projects/timeline-stack/timeline/deps/plug/lib/plug/debugger.ex:136: TimelineWeb.Endpoint."call (overridable 3)"/2
(timeline 0.1.0) lib/timeline_web/endpoint.ex:1: TimelineWeb.Endpoint.call/2
(phoenix 1.7.10) 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) /Users/sergio/Projects/timeline-stack/timeline/deps/cowboy/src/cowboy_handler.erl:37: :cowboy_handler.execute/2
(cowboy 2.10.0) /Users/sergio/Projects/timeline-stack/timeline/deps/cowboy/src/cowboy_stream_h.erl:306: :cowboy_stream_h.execute/3
(cowboy 2.10.0) /Users/sergio/Projects/timeline-stack/timeline/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
I looked through the pipeline & other related places, and found no references to the usual flash plugs, so I had no idea why flash was being referenced here at all. Only later on I realized that:
- The create function had a verified route for another function in the same controller, which was update
- That function did call put_flash/2
def update(conn, %{"token" => token}) do
case Accounts.confirm_user(token) do
{:ok, _} ->
conn
# |> put_flash(:info, "User confirmed successfully.")
|> redirect(to: ~p"/")
:error ->
# If there is a current user and the account was already confirmed,
# then odds are that the confirmation link was already visited, either
# by some automation or by the user themselves, so we redirect without
# a warning message.
case conn.assigns do
%{current_user: %{confirmed_at: confirmed_at}} when not is_nil(confirmed_at) ->
redirect(conn, to: ~p"/")
%{} ->
conn
# |> put_flash(:error, "User confirmation link is invalid or it has expired.")
|> redirect(to: ~p"/")
end
end
end
After removing that reference to put_flash, the error was gone (reasonably).
What I’m wondering is the mechanism that caused that other controller function to emit this error in the first place? I suspect the compiler is doing a check on that verified route, but I thought they only checked the validity of the route, not the plugs called within that route?
It’s behaving as if it did a runtime check of the controller method itself, and I’m unclear on how that works. Why does “put_flash/2” in a verified route seem to be triggered when running that verification?
Thank you! Note that I’ve fixed the error, just curious about how the framework works in this regard.