Custom template_not_found callback in ErrorView for REST API

I’m trying to build a REST API using Phoenix for the first time, and I just want to customize the error view to make them all render a JSON in the browser.

By default, the ErrorView module has a template_not_found callback which returns the status message.I followed the example from elixir-phoenix-realworld-example-app, and here is my error_view.ex:

defmodule DictletWeb.ErrorView do
  use DictletWeb, :view

  def render("404.json", _assigns) do
    %{
      "error" => %{
        "code" => 404,
        "msg" => "Page Not Found"
      }
    }
  end

  def render("500.json", _assigns) do
    %{
      "error" => %{
        "code" => 500,
        "msg" => "Internal Error"
      }
    }
  end

  def template_not_found(_template, assigns) do
    render("404.json", assigns)
  end
end

I have set debug_errors: false in the config, but when I visit non-existent route in the browser, I get errors below:

[info] GET /random_path
[debug] Converted error Phoenix.Router.NoRouteError to 404 response
[error] #PID<0.758.0> running DictletWeb.Endpoint (connection #PID<0.735.0>, stream id 2) terminated
Server: localhost:4000 (http)
Request: GET /random_path
** (exit) an exception was raised:
    ** (Protocol.UndefinedError) protocol Phoenix.HTML.Safe not implemented for %{"error" => %{"code" => 404, "msg" => "Page Not Found"}} of type Map. This protocol is implemented for the following type(s): Phoenix.LiveView.Comprehension, Phoenix.LiveView.Component, Phoenix.LiveView.Rendered, Phoenix.LiveComponent.CID, Float, Date, Time, NaiveDateTime, Tuple, List, Phoenix.HTML.Form, BitString, DateTime, Integer, Atom
        (phoenix_html 2.14.3) lib/phoenix_html/safe.ex:1: Phoenix.HTML.Safe.impl_for!/1
        (phoenix_html 2.14.3) lib/phoenix_html/safe.ex:15: Phoenix.HTML.Safe.to_iodata/1
        (phoenix 1.5.12) lib/phoenix/controller.ex:777: Phoenix.Controller.render_and_send/4
        (phoenix 1.5.12) lib/phoenix/endpoint/render_errors.ex:78: Phoenix.Endpoint.RenderErrors.instrument_render_and_send/5
        (phoenix 1.5.12) lib/phoenix/endpoint/render_errors.ex:64: Phoenix.Endpoint.RenderErrors.__catch__/5
        (phoenix 1.5.12) lib/phoenix/endpoint/cowboy2_handler.ex:65: Phoenix.Endpoint.Cowboy2Handler.init/4
        (cowboy 2.9.0) /home/tunkshif/Documents/Project/dictlet/deps/cowboy/src/cowboy_handler.erl:37: :cowboy_handler.execute/2
        (cowboy 2.9.0) /home/tunkshif/Documents/Project/dictlet/deps/cowboy/src/cowboy_stream_h.erl:306: :cowboy_stream_h.execute/3
        (cowboy 2.9.0) /home/tunkshif/Documents/Project/dictlet/deps/cowboy/src/cowboy_stream_h.erl:295: :cowboy_stream_h.request_process/3
        (stdlib 3.15) proc_lib.erl:226: :proc_lib.init_p_do_apply/3

It seems that the map returned from render("404.json", _assigns) was treated as HTML, but I want it to be rendered as JSON in the browser. So how can I fix this?

Are you not expecting any HTML. from this app?

You may check your router/endpoint module for accept plugs and remove html from it so you signal that you won’t be giving any HTML back.

Also, browsers usually send Accept: text/html header which signals your app that they need HTML response. Use curl or any other tool to send a request with Accept: application/json header to see if it makes any difference.

1 Like

Thanks for your reply!
I checked my router module and I do have an accept plug for only json. I tested it again using curl specifying the header with Accept: application/json, and I do get the expected json response.
Sure I’m not expecting any HTML response from the server, is there a way that even if I don’t specify the request header the server will always respond in json?

I actually don’t know, because I didn’t need it ever.

In my cases, I never needed to call my api endpoints from browser “directly”. They were to be used by XHR requests or a code calling that endpoint, which in both cases JSON response is sent correctly.

I don’t know your use case, but generally it is not a problem.

Yeah, I know that the API may never be called directly through the browser, sometimes I just like to visit the local server in the browser to get a quick test. Actually this is also not a big problem for me.
Anyway, thanks for your help!

欢迎.

The Phoenix.Controller.accepts/2 function docs say that

It is important to notice that browsers have historically sent bad accept headers. For this reason, this function will default to “html” format whenever:

  • the accepted list of arguments contains the “html” format
  • the accept header specified more than one media type preceded or followed by the wildcard media type “*/*

If you’re visiting from your browser just for a quick test, it’s going to tell the server that it accepts html, which seems to override the explicit plug :accepts, ["json"] config.

Luckily you can force it, also described in the same doc section above, by replacing the :accepts with plug :put_format, "json" so that should always render json regardless.

1 Like

谢谢!Thanks for your help!
I’ve tried what you said but unfortunately it still didn’t work.
However, according to the information you provided, I‘ve found that the rendering of ErrorView is handled by the :render_errors config in Phoenix.Endpoint.
After removing html from accepts, now it works as expected.

# config/config.exs
config :dictlet, DictletWeb.Endpoint,
  render_errors: [view: DictletWeb.ErrorView, accepts: ~w(html json), layout: false],
1 Like