404 is rendered as 500 in staging and production

I have route say /status

If I hit the api /status it gives 200 which is fine.
Now when I hit /status1 it gives 500 in production but 404 in dev.

I tried spotting the difference and found that if I change my prod.exs file as

config :my_app, MyApp.Web.Endpoint,
  load_from_system_env: true,
  url: [port: 4000],
  server: true,
  debug_errors: true // introduce this

It is working as expected but I read somewhere we shouldn’t add debug_errors in prod.
Can you please suggest on this.

Can anyone look into this.

Thanks.

Can you please share the logs? The NoRouteError exception should be converted to 404, so it may be something else that’s exploding on production.

1 Like

The complete log

12:07:43.662 [error] #PID<0.687.0> running Diamond.Web.Endpoint (connection #PID<0.686.0>, stream id 1) terminated
Server: localhost:4000 (http)
Request: GET /status1
** (exit) an exception was raised:
    ** (UndefinedFunctionError) function Diamond.Web.ErrorView.render/2 is undefined (module Diamond.Web.ErrorView is not available)
        Diamond.Web.ErrorView.render("404.json", %{conn: %Plug.Conn{adapter: {Plug.Cowboy.Conn, :...}, assigns: %{kind: :error, layout: false, reason: %Phoenix.Router.NoRouteError{conn: %Plug.Conn{adapter: {Plug.Cowboy.Conn, :...}, assigns: %{}, before_send: [#Function<1.112466771/1 in Plug.Logger.call/2>, #Function<0.84352368/1 in NewRelic.Phoenix.Transaction.Plug.call/2>, #Function<0.83136174/1 in NewRelic.Transaction.Plug.call/2>], body_params: %{}, cookies: %Plug.Conn.Unfetched{aspect: :cookies}, halted: false, host: "localhost", method: "GET", owner: #PID<0.687.0>, params: %{}, path_info: ["status1"], path_params: %{}, port: 4000, private: %{Diamond.Web.Router => {[], %{}}, :newrelic_tx_instrumented => true, :phoenix_endpoint => Diamond.Web.Endpoint, :phoenix_router => Diamond.Web.Router}, query_params: %{}, query_string: "", remote_ip: {0, 0, 0, 0, 0, 0, 0, 1}, req_cookies: %Plug.Conn.Unfetched{aspect: :cookies}, req_headers: [{"accept", "*/*"}, {"accept-encoding", "gzip, deflate, br"}, {"cache-control", "no-cache"}, {"connection", "keep-alive"}, {"host", "localhost:4000"}, {"postman-token", "b738f1b4-2bf0-4388-adae-0f46b07ac005"}, {"user-agent", "PostmanRuntime/7.26.3"}], request_path: "/status1", resp_body: nil, resp_cookies: %{}, resp_headers: [{"cache-control", "max-age=0, private, must-revalidate"}, {"access-control-allow-origin", "*"}, {"access-control-expose-headers", ""}, {"access-control-allow-credentials", "true"}], scheme: :http, script_name: [], secret_key_base: :..., state: :unset, status: nil}, message: "no route found for GET /status1 (Diamond.Web.Router)", plug_status: 404, router: Diamond.Web.Router}, stack: [{Diamond.Web.Router, :__match_route__, 4, [file: 'lib/diamond_web/router.ex', line: 1]}, {Diamond.Web.Router, :"call (overridable 2)", 2, [file: 'lib/phoenix/router.ex', line: 304]}, {Diamond.Web.Router, :call, 2, [file: 'lib/plug/error_handler.ex', line: 65]}, {Diamond.Web.Endpoint, :plug_builder_call, 2, [file: 'lib/diamond_web/endpoint.ex', line: 1]}, {Diamond.Web.Endpoint, :call, 2, [file: 'lib/diamond_web/endpoint.ex', line: 1]}, {Phoenix.Endpoint.Cowboy2Handler, :init, 2, [file: 'lib/phoenix/endpoint/cowboy2_handler.ex', line: 34]}, {:cowboy_handler, :execute, 2, [file: '/Users/sahilpaudel/Documents/PharmEasy/Elixir/diamond/deps/cowboy/src/cowboy_handler.erl', line: 41]}, {:cowboy_stream_h, :execute, 3, [file: '/Users/sahilpaudel/Documents/PharmEasy/Elixir/diamond/deps/cowboy/src/cowboy_stream_h.erl', line: 320]}]}, before_send: [#Function<1.112466771/1 in Plug.Logger.call/2>, #Function<0.84352368/1 in NewRelic.Phoenix.Transaction.Plug.call/2>, #Function<0.83136174/1 in NewRelic.Transaction.Plug.call/2>], body_params: %{}, cookies: %Plug.Conn.Unfetched{aspect: :cookies}, halted: false, host: "localhost", method: "GET", owner: #PID<0.687.0>, params: %{}, path_info: ["status1"], path_params: %{}, port: 4000, private: %{Diamond.Web.Router => {[], %{}}, :newrelic_tx_instrumented => true, :phoenix_endpoint => Diamond.Web.Endpoint, :phoenix_format => "json", :phoenix_layout => false, :phoenix_router => Diamond.Web.Router, :phoenix_template => "404.json", :phoenix_view => Diamond.Web.ErrorView}, query_params: %{}, query_string: "", remote_ip: {0, 0, 0, 0, 0, 0, 0, 1}, req_cookies: %Plug.Conn.Unfetched{aspect: :cookies}, req_headers: [{"accept", "*/*"}, {"accept-encoding", "gzip, deflate, br"}, {"cache-control", "no-cache"}, {"connection", "keep-alive"}, {"host", "localhost:4000"}, {"postman-token", "b738f1b4-2bf0-4388-adae-0f46b07ac005"}, {"user-agent", "PostmanRuntime/7.26.3"}], request_path: "/status1", resp_body: nil, resp_cookies: %{}, resp_headers: [{"cache-control", "max-age=0, private, must-revalidate"}, {"access-control-allow-origin", "*"}, {"access-control-expose-headers", ""}, {"access-control-allow-credentials", "true"}], scheme: :http, script_name: [], secret_key_base: :..., state: :unset, status: 404}, kind: :error, reason: %Phoenix.Router.NoRouteError{conn: %Plug.Conn{adapter: {Plug.Cowboy.Conn, :...}, assigns: %{}, before_send: [#Function<1.112466771/1 in Plug.Logger.call/2>, #Function<0.84352368/1 in NewRelic.Phoenix.Transaction.Plug.call/2>, #Function<0.83136174/1 in NewRelic.Transaction.Plug.call/2>], body_params: %{}, cookies: %Plug.Conn.Unfetched{aspect: :cookies}, halted: false, host: "localhost", method: "GET", owner: #PID<0.687.0>, params: %{}, path_info: ["status1"], path_params: %{}, port: 4000, private: %{Diamond.Web.Router => {[], %{}}, :newrelic_tx_instrumented => true, :phoenix_endpoint => Diamond.Web.Endpoint, :phoenix_router => Diamond.Web.Router}, query_params: %{}, query_string: "", remote_ip: {0, 0, 0, 0, 0, 0, 0, 1}, req_cookies: %Plug.Conn.Unfetched{aspect: :cookies}, req_headers: [{"accept", "*/*"}, {"accept-encoding", "gzip, deflate, br"}, {"cache-control", "no-cache"}, {"connection", "keep-alive"}, {"host", "localhost:4000"}, {"postman-token", "b738f1b4-2bf0-4388-adae-0f46b07ac005"}, {"user-agent", "PostmanRuntime/7.26.3"}], request_path: "/status1", resp_body: nil, resp_cookies: %{}, resp_headers: [{"cache-control", "max-age=0, private, must-revalidate"}, {"access-control-allow-origin", "*"}, {"access-control-expose-headers", ""}, {"access-control-allow-credentials", "true"}], scheme: :http, script_name: [], secret_key_base: :..., state: :unset, status: nil}, message: "no route found for GET /status1 (Diamond.Web.Router)", plug_status: 404, router: Diamond.Web.Router}, stack: [{Diamond.Web.Router, :__match_route__, 4, [file: 'lib/diamond_web/router.ex', line: 1]}, {Diamond.Web.Router, :"call (overridable 2)", 2, [file: 'lib/phoenix/router.ex', line: 304]}, {Diamond.Web.Router, :call, 2, [file: 'lib/plug/error_handler.ex', line: 65]}, {Diamond.Web.Endpoint, :plug_builder_call, 2, [file: 'lib/diamond_web/endpoint.ex', line: 1]}, {Diamond.Web.Endpoint, :call, 2, [file: 'lib/diamond_web/endpoint.ex', line: 1]}, {Phoenix.Endpoint.Cowboy2Handler, :init, 2, [file: 'lib/phoenix/endpoint/cowboy2_handler.ex', line: 34]}, {:cowboy_handler, :execute, 2, [file: '/Users/sahilpaudel/Documents/PharmEasy/Elixir/diamond/deps/cowboy/src/cowboy_handler.erl', line: 41]}, {:cowboy_stream_h, :execute, 3, [file: '/Users/sahilpaudel/Documents/PharmEasy/Elixir/diamond/deps/cowboy/src/cowboy_stream_h.erl', line: 320]}], view_module: Diamond.Web.ErrorView, view_template: "404.json"})
        (phoenix) lib/phoenix/view.ex:399: Phoenix.View.render_to_iodata/3
        (phoenix) lib/phoenix/controller.ex:729: Phoenix.Controller.__put_render__/5
        (diamond) lib/diamond_web/endpoint.ex:1: Diamond.Web.Endpoint.instrument/4
        (phoenix) lib/phoenix/endpoint/render_errors.ex:75: 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:34: Phoenix.Endpoint.Cowboy2Handler.init/2
        (cowboy) /Users/sahilpaudel/Documents/PharmEasy/Elixir/diamond/deps/cowboy/src/cowboy_handler.erl:41: :cowboy_handler.execute/2

Here’s the relevant line:

Do you have the Diamond.Web.ErrorView module defined? Is it configured under render_errors in the endpoint config?

Can you please share the complete configs for dev and prod?

Diamond.Web.ErrorView it wasn’t defined.

Also there was no view defined in the diamond_web.ex file.
I went ahead and added this lines.

def view do
    quote do
      use Phoenix.View,
        root: "lib/diamond_web/templates",
        namespace: Diamond.Web
    end
  end

And my Diamond.Web.ErrorView looks like

defmodule Diamond.Web.ErrorView do
  use Diamond.Web, :view

  def render("404.json", _assigns) do
    %{errors: %{detail: "Page not found"}}
  end

  def render("500.json", _assigns) do
    %{errors: %{detail: "Internal server error"}}
  end

  # In case no render clause matches or no
  # template is found, let's render it as 500
  def template_not_found(_template, assigns) do
    render("500.json", assigns)
  end
end

And now everything is working as expected but I am not sure if this is the approach.

This is exactly the right approach. Not sure why your codebase diverged from that, but here are the relevant fragments from a freshly generated Phoenix API project (--no-html --no-webpack):

# config/dev.exs
config :test, TestWeb.Endpoint,
  render_errors: [view: TestWeb.ErrorView, accepts: ~w(json), layout: false],
  # ...
# lib/test_web/views/error_view.ex
defmodule TestWeb.ErrorView do
  use TestWeb, :view

  # If you want to customize a particular status code
  # for a certain format, you may uncomment below.
  # def render("500.json", _assigns) do
  #   %{errors: %{detail: "Internal Server Error"}}
  # end

  # By default, Phoenix returns the status message from
  # the template name. For example, "404.json" becomes
  # "Not Found".
  def template_not_found(template, _assigns) do
    %{errors: %{detail: Phoenix.Controller.status_message_from_template(template)}}
  end
end
3 Likes

Can you please tell me how the view should be defined in diamond_web.ex as I see different approaches around the internet.

The one generated by default is perfectly fine. Are there any concerns that you’d like to address?

Phoenix is not really an opinionated framework, so you can structure your web folder either by module kind (controller, view, etc) or by business use case (registration, user profile, etc.).

1 Like

No nothing as of now.

Thanks a ton for the help :slight_smile: