phoenixframework/phoenix

Unclear how to handle errors in Phoenix.Endpoint

mitchellhenke opened this issue ยท 4 comments

Environment

  • Elixir version (elixir -v): 1.6.2
  • Phoenix version (mix deps): 1.3.1
  • Operating system: macOS 10.13.3

Apologies ahead of time as this is an issue that is difficult for me to explain, so this is a bit rambly ๐Ÿ™‚

This originated from getsentry/sentry-elixir#229

Currently, to catch errors in Phoenix during the request process, using Plug.ErrorHandler in the Router works well. The limitation being if the error happens before the request reaches the Router in the Endpoint, and I haven't been able to find a way I like to catch them.

Since Phoenix.Endpoint defines an overridable call/2 here I was thinking I could override and include my own error handling code by adding something like:

# lib/my_app_web/endpoint.ex
# ...
def call(conn, opts) do
  IO.inspect(conn.private)
  conn = put_in conn.secret_key_base, config(:secret_key_base)
  conn = put_in conn.script_name, script_name()
  conn = Plug.Conn.put_private(conn, :phoenix_endpoint, __MODULE__)

  try do
    super(conn, opts)
  catch
    kind, reason ->
      stack = System.stacktrace()
      MyErrorHandlingService.report(kind, reason, stack)
      Phoenix.Endpoint.RenderErrors.__catch__(conn, kind, reason, @phoenix_render_errors)
  end
end

# => %{phoenix_endpoint: MyAppWeb.Endpoint}

This works in that my error handling service gets called. The issue with it is it seems like both the original call/2 that Phoenix.Endpoint defines gets called first, and then mine gets called. It seems that way because the inspect above prints out :private with the endpoint module before my code puts it in there.

I also tried to use Plug.ErrorHandler in the Endpoint, but any handle_errors/2 that get defined don't get called. I'm guessing because Phoenix.Endpoint works similarly in defining an overridable call/2 function?

Expected behavior

I'm not sure how I would expect to be able to handle the errors. I'm happy to try solutions I have missed or put together a PR.

If there isn't a good way currently, being able to define a function to handle errors in the Phoenix.Endpoint catch could work (similar to Plug.ErrorHandler), but I'm open to anything that makes it easier ๐Ÿ™‚

Phoenix' call is happening in a @before_compile, so you need to make sure to register your hook in a before compile too. That's basically what Plug.ErrorHandler does (so I would recommend to even not depend on it and do your own try/catch block, see Plug.ErrorHandler source).

Also note that you shouldn't replicate what Phoenix does, because you would be fiddling with Phoenix internals and that's error prone. :) Although you probably only did that for experimentation purposes.

Here is the building block that you want:

defmodule Sample do
  defmacro __using__(_opts) do
    quote do
      @before_compile Sample
    end
  end

  defmacro __before_compile__(_) do
    quote do
      defoverridable call: 2

      def call(conn, opts) do
        try do
          super(conn, opts)
        catch
          kind, reason ->
            Sentry.stuff(...)
            :erlang.raise(kind, reason, System.stacktrace)
        end
      end
    end
  end
end

It should work for plugs, phoenix, etc.

@josevalim thanks for the quick response! that works perfectly ๐Ÿ’–

@josevalim Can we use the same to tackle this problem? absinthe-graphql/absinthe#1219
Problem would be Absinthe context would be gone at this point