Rendering 404s through LiveView

I am currently in the process of converting a Turbolinks style application to LivewView and one issue I run into is how to handle non 2xx responses with LiveView.

Let’s say I have a “live” route containing a parameter /hello/:name and I validate the name in the handle_params callback;

def handle_params(params, _uri, socket) do
    case check_valid_name(params["name"]) do
      {:ok, name} ->
        # Just render the LiveView
        {:noreply, socket}

      {:error, reason} ->
        # Render 404 response as the name is invalid.
    end
  end

I could check the name and return a 404 through the conn instance within a controller that renders the LiveView. But then I don’t have the ability to navigate within the LiveView itself through live_patch.

I can’t find anything about this in the LiveView docs or “official” examples. Any help is really appreciated!

3 Likes

I display a flash message for situations like this and redirect to an index view, e.g. in your {:error, reason} case:

      {:ok, socket
        |> put_flash(:error, msg)
        |> push_redirect(to: Routes.live_path(socket, ReactionWeb.AnalysisProject.Index))
      }

I haven’t figured out how to clear flash messages in my app template without an http round trip, but I’m in a no-mans land between 0.9.0 and 0.10.0 and that was cleared up with the live templates.

This approach may or may not be useful for you, but it works for my use case.

1 Like

There was a similar discussion in the issues tracker here.

The proposed solution was:

You can have a plug in your router that handles these cases or you can raise an exception on LiveView mount and make the exception be treated as 404 by implementing Plug.Exception.

3 Likes

There’s a phx-click="lv:clear-flash" event that you could add to your flash alerts as an interactive way to clear it on click. Or have any other callback that doesn’t do a redirect or patch (these two clear any previously set flash automatically) and doesn’t set a flash, actually clear it.

{:ok, socket
  |> clear_flash()
2 Likes

Yes works like a charm :ok_hand:

To help others finding this threat;

def handle_params(params, _uri, socket) do
  case check_valid_name(params["name"]) do
    {:ok, name} ->
      # Just render the LiveView
      {:noreply, socket}

    {:error, reason} ->
      # Render 404 response as the name is invalid.
      raise ExampleWeb.UserLive.InvalidNameError
  end
end

defmodule ExampleWeb.UserLive.InvalidNameError do
  defexception message: "invalid name", plug_status: 404
end
15 Likes

Thanks @sfusato!

I had seen the clear_flash() but I hadn’t seen the built in "lv:clear-flash" - thanks for the heads up on that. Unfortunately neither will work for me until I port my app layouts to use liveview. I’m a couple of versions behind (0.9.0) - things are moving really quickly right now so I’ll probably wait a couple more weeks until the Phoenix / LiveView settles before rejigging the app templates to work properly.

1 Like

The custom Error trick is working fine in production.

But I cannot manage to make my LiveView to return a 404 status code during tests.
I would like to write my test like this, but it’s not working because the LiveView is raising.

test "404 on unknown entry", %{conn: conn} do
  assert get(conn, "/wrong_path") |> response(404)
end

So I wrote this, which is working.
What really bothers me is that whatever status code I pass to response/2, the test will still pass.

test "404 on unknown entry", %{conn: conn} do
  assert_raise PhxLiveStorybook.EntryNotFound, fn ->
    get(conn, "/storybook/wrong") |> response(404)
  end
end

How do you tests your 404?

2 Likes

Unfortunately the approach in this example, raising in the handle_params callback, does not work as expected in current versions of LV. As documented here it appears that errors raised outside of mount will not be handled automatically by plug, which is tricky especially when dealing with LiveComponents which do not have access to any data inside their mount callback. If you follow the pattern in which records are fetched in the update callback with an ID coming from a URL, the LV process will continuously crash instead of showing a 404 error view.

Edit:

Looking at this example from the LiveBeats source it looks like best practice might be to use a separate LiveView for each “resource” route you want to support, rather than fetching them inside a live component as the docs imply. The rule of thumb would be that you should only put state in a live component if it’s still possible/desirable to render the parent live view if something about that state prevents it from rendering. You never want to design things in such a way that an invalid url will cause a child component to crash, because the parent LV will continuously reload and crash instead of recovering.

1 Like

LiveView will translate errors in the entire mount life cycle (including handle_params) to plug_status during the dead render, so handle_params is covered the same way mount is :slight_smile:

1 Like

That’s weird. I wonder what I am doing wrong, since I am pretty sure I am observing different behavior raising in my handle_params handler vs in mount. In mount I see the Phoenix debugging page rendered by Plug, but in handle_params the view just remounts and crashes again.

Edit:

I figured this out. I had configured my handle_params functions in such a way that the implementation containing the get! was invoked only after initial render. Once I removed a guard preventing it from being called initally I get the plug error as expected.

Unfortunately it seems that this means that I’m not able to rely on that behavior as the get! depends on info not present in the initial set of params, so I will need to either manually handle the missing record case with a redirect or something, or figure out how to move the missing info into the url itself.

1 Like

You can still rely on the Ecto.NoResultsError (or any error that implements the plug_status protocol) even for live navigation. We detect a failed mount and the client will fallback to force reloading the page in such scenarios, so your “regular/dead” 404 code paths will be invoked on a live navigate that triggers a 404

1 Like

I haven’t run into any problems with nagivation. The case where I had a problem was after a full page refresh with callbacks that look something like this:

  @impl true
  def mount(_params, _session, socket) do
    {:ok, assign(socket, :loading, !connected?(socket))}
  end

  @impl true
  def handle_params(%{"id" => id}, _, socket = %{assigns: %{loading: false}}) do
    {:noreply,
     socket
     |> assign(:page_title, page_title(socket.assigns.live_action))
     |> assign(:user, Accounts.get_user!(id, prefix: assigns[:prefix]))}
  end

id is a route path param, whereas prefix is an optional query param. If I remove the loading guard, errors are handled properly, but the prefix does not appear to be added to params yet, so the query can never succeed. Perhaps there’s a better way to ensure the query has access to the query param it needs?

I ran into this and also couldn’t get this to be correctly rescued in tests; I’m porting a large non-live-view app over to live view and we have a ton of tests asserting assert html_response(conn, 404) and it appears they all have to be patched to assert_raise, which is a shame.

I assume you didn’t find a solution on this front?

Yeah, I define custom exceptions and do assert_raise on them.
Example here for the custom exception and the test

Yeah that’s what I’m doing too - it just seems like a bug that the endpoint having debug_errors: false on it correctly renders a 404/500 template, yet in the tests, you have to rescue the error yourself and can’t test that error template. Appreciate the quick answer!

1 Like