1
votes

Running into a problem with Phoenix when a user attempts to hit an API route that doesn't exist. Phoenix appears to be looking to render "404.html" by default, resulting in a catch all render function being called (which returns a map) which then results in an error because Phoenix is attempting to render JSON as HTML.

** (UndefinedFunctionError) function Phoenix.HTML.Safe.to_iodata/1 is undefined (module Phoenix.HTML.Safe is not available)
        Phoenix.HTML.Safe.to_iodata(%{error: %{errors: ["An internal error has occurred. Our team has been notified."], message: "Internal Error"}})
        (phoenix) lib/phoenix/controller.ex:729: Phoenix.Controller.__put_render__/5
        (phoenix) lib/phoenix/endpoint/render_errors.ex:77: Phoenix.Endpoint.RenderErrors.instrument_render_and_send/5
        (phoenix) lib/phoenix/endpoint/render_errors.ex:62: Phoenix.Endpoint.RenderErrors.__catch__/5
        (phoenix) lib/phoenix/endpoint/cowboy2_handler.ex:42: Phoenix.Endpoint.Cowboy2Handler.init/4

This is the default render function that is ultimately being called:

  def render(code, _assigns) do
    Logger.error("Error handler requested #{inspect code}. Please add support for that.")

    %{
      error: %{
        message: "Internal Error",
        errors: ["An internal error has occurred. Out team has been notified."]
      }
    }
  end

I have seen "set_format" solution that was in a previous question (here: https://stackoverflow.com/a/39189452) and, as the user states, this does feel a bit hacky, though it does appear to work.

I am also aware that I could just specify a render function that accepts 404.html but that also feels like a bit more of a bandaid than I would like.

My ultimate goal is to get Phoenix to not want to render 404.html, rather I would like to either arbitrarily specify the template, or at the least, tell Phoenix it should look for 404.json instead. Ideally, the solution would be application wide.

2

2 Answers

2
votes

Your router is just a plug. You can handle errors in the same way as Plug.Router or any plug does. In the sample code below, feel free to replace MyAppWeb with the name of your web module.

defmodule MyAppWeb.Router do
  use MyAppWeb, :router
  use Plug.ErrorHandler  #<-- Add this line

  # and implement the callback handle_errors/2
  defp handle_errors(conn, %{reason: %Phoenix.Router.NoRouteError{message: message}}) do
    conn |> json(%{error: message}) |> halt()
  end

  defp handle_errors(conn, _) do
    conn |> json(%{error: "unknown"}) |> halt()
  end

  ...
end

Note that you can call all the functions that are available to a controller, because Phoenix.Controller is imported when you import MyAppWeb, :router. You can see this in the source code of the module MyAppWeb, pay attention to the macro __using__/1 and the function router/0.

0
votes

You should configure your endpoint to render errors as json:

config :the_app, TheApp.Endpoint,
  render_errors: [view: TheApp.ErrorView, accepts: ~w(json), layout: false]