Curious behavior from verified routes, what's happening here?

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:

  1. The create function had a verified route for another function in the same controller, which was update
  2. 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.

It’s emitted from the put_flash function, nothing to do with verified routes. See here.

1 Like

Understood, but why is that function being triggered, if it’s in a completely different route?

That’s the question at hand: the put_flash function is in a different function in that module, so there is no reason that error should trigger?

I would argue that you shouldn’t use flash at all if you’re trying to port phx.gen.auth as a JSON endpoint. Yes, you do need to ensure that the HTTP response headers are and that the cookie is provided, but flash is really something that’s dedicated to inform the user that something happened in one instance.

You could easily also argue that the token you get back shouldn’t be a cookie, but rather part of the JSON response or in the Authorization header in the response

In this instance, I don’t think it’s entirely unreasonable to push the flash responsibility to something else.

I’m not going to use flash at all! I’m aware it’s not useful and in the process of removing it all, as stated in the post. I’m curious however about the mechanism as to why a flash function (let behind by mistake) in another route would cause an error elsewhere. :slight_smile:

The curious behavior is this: I called route_1 which seemingly has no relation to route_2, but got an error related to a function in route_2. It’s not a compile time error too, rather a runtime one. Why?

This seems to suggest you update route to be accessed via http. Hard to say what called it though.

1 Like

Ah crap, sorry, I didn’t read your question properly.

Is it the exact same stack trace or you were simply seeing a warning?

That was what I noticed too, something is POSTing to the confirmation endpoint route, might it be phx.gen.auth tests?

Nope–that was me :slight_smile: that’s the endpoint I was hitting to test out. But it’s a separate view from the one the function put_flash was used in.

That’s the stack trace, yes :slight_smile:

But is it the exact stacktrace with line numbers and everything? In a fresh, non-LiveView phx_gen_auth invocation, line 19 of user_confirmation_controller.ex is a call to put_flash which is not present in your first snippet. I assume you removed it, but is it possible you just hadn’t saved the file or something when you originally got the error? Otherwise, put_flash in the update action is line 37.

Just trying to understand because otherwise it makes no sense. There is nothing I can see about verified routes that would rewrite code in such a crazy way that it would cause a runtime exception in a function that isn’t being called.

That’s a great point, I’ll try to replicate this again with a different controller that’s been untouched.