My user got an 'Internal Server Error' at a specific URL. I check the server logs - nothing. This is not the first time this has happened. So I spend a whole day looking for a solution and chatting about it. I try out my usual tricks like turning on level: :debug
for logging. Sometimes Ecto errors are hidden because of that.
Curiously sometimes the error shows up. But after a hard refresh on the browser - again nothing. Just
[info] GET /learn
[info] Sent 500 in 2ms
Where's the error?
Solution
I was advised to use Sentry, so I dug in and tried to figure out what they do differently. Their elixir integration is open-source so I found the plug that's responsible for capturing errors and implemented that into my own app.
Amazingly it actually logs the hidden error. So this time the problem was actually Phoenix Endpoint not handling its error properly.
Here's my module.
defmodule PlatformWeb.EndpointErrorCatcher do
@moduledoc """
Since Phoenix doesn't capture all errors we've got to try-catch them ourselves. I have no idea how Phoenix is meant to be working in this regard, but I just had an invisible error in prod.
Module is ripped from https://github.com/getsentry/sentry-elixir/blob/02a2cc31c7444af88d079f7c32a789b9b1b4e6f1/lib/sentry/plug_capture.ex
#### Usage
In a Phoenix application, it is important to use this module before
the Phoenix endpoint itself. It should be added to your endpoint.ex:
defmodule MyApp.Endpoint
use EndpointErrorCatcher
use Phoenix.Endpoint, otp_app: :my_app
# ...
end
In a Plug application, it can be added below your router:
defmodule MyApp.PlugRouter do
use Plug.Router
use EndpointErrorCatcher
# ...
end
"""
defmacro __using__(_opts) do
quote do
@before_compile PlatformWeb.EndpointErrorCatcher
end
end
defmacro __before_compile__(_) do
quote do
defoverridable call: 2
def call(conn, opts) do
try do
super(conn, opts)
rescue
e in Plug.Conn.WrapperError ->
stack =
e.stack
|> List.first()
|> elem(3)
|> then(fn [file: file, line: line] -> "#{file}:#{line}" end)
Logger.error("EndpointWrapperError at #{stack}: #{e.reason.message}")
Plug.Conn.WrapperError.reraise(e)
e ->
stack =
__STACKTRACE__
|> List.first()
|> elem(3)
|> then(fn [file: file, line: line] -> "#{file}:#{line}" end)
Logger.error("EndpointUnknownError at #{stack}: #{e.reason.message}")
:erlang.raise(:error, e, __STACKTRACE__)
catch
kind, reason ->
message = "Uncaught #{kind} - #{inspect(reason)}"
stack = __STACKTRACE__
Logger.error(message, stacktrace: stack)
:erlang.raise(kind, reason, stack)
end
end
end
end
end
It logs the file:line and reason for error.
Use it as per instructed in @moduledoc
.