Handle Phoenix Exceptions gracefully

- 4 min read

Hello there fellow Elixirists!

It’s been a while since my last blog post which is largely the fault of my new startup indiecourses.com which takes up all my previous blog-writing time!

To show my apologies, please accept the following dad joke:

I got pulled over by a police officer.He looked at my license and said I should wear glasses.
I said I have contacts.

He said he doesn’t care who I know.

Alright! Let’s talk about Exceptions in Phoenix and how we can make them a bit less noisy.

The Problem

When you run a Phoenix application in production, you might have seen exceptions like this one:

** (Phoenix.NotAcceptableError) no supported media type in accept header.
Expected one of ["html"] but got the following formats:

  * "text/plain" with extensions: ["txt", "text"]

To accept custom formats, register them under the :mime library
in your config/config.exs file:

    config :mime, :types, %{
      "application/xml" => ["xml"]
    }

And then run `mix deps.clean --build mime` to force it to be recompiled.

or this one:

** (Plug.CSRFProtection.InvalidCSRFTokenError) invalid CSRF (Cross Site Request Forgery) token, please make sure that:
  * The session cookie is being sent and session is loaded
  * The request include a valid '_csrf_token' param or 'x-csrf-token' header

Phoenix throws these exceptions when it protects you from invalid - and potentially malicious - requests. The first exception occurs when a user tries to acces your website through e.g. a script and forgot to set a correct Mime type in the Accept request header. The second exception occurs when somebody tries to submit a form without the proper CSRF token which protects you from forgery.

It’s great that Phoenix protects us from such bad requests, but how can we make it a bit less noisy? Ideally, we would not receive exception alerts whenever a user makes a bad request.

The Solution

The solution is quite simple: We can implement the Plug.Exception protocol for the Phoenix errors and this will convert the exception into a 400 bad request response status.

All we have to do is to put these lines into e.g. our error_html.ex file:

# Return a 400 instead of raising an Exception if a request has# the wrong Mime format (e.g. "text")
defimpl Plug.Exception, for: Phoenix.NotAcceptableError do
  def status(_exception), do: 400
  def actions(_exception), do: []
end

# Return a 400 instead of raising an Exception if a request has
# an invalid CSRF token.
defimpl Plug.Exception, for: Plug.CSRFProtection.InvalidCSRFTokenError do
  def status(_exception), do: 400
  def actions(_exception), do: []
end

And that’s it! The user will now receive a 400 response status, our application won’t throw an error, and our error tracker won’t send us email alerts about noisy exceptions anymore. Great!

Like what you read? Sign up for more!

Testing

You can’t trust your code unless you tested it, so let’s write some tests that proof that our exceptions are now returned as proper 400 bad request responses.

These are the two tests you need:

test "returns 400 for invalid Mime formats", %{conn: conn} do  assert_error_sent 400, fn ->
    conn
    |> put_req_header("accept", "text/plain")
    |> get(~p"/")
  end
end

test "returns 400 for invalid CSRF tokens", %{conn: conn} do
  assert_error_sent 400, fn ->
    conn
    # This private flag is set in our tests and disables the CSRF protection.
    # We need to enable it manually if we want to test the CSRF protection.
    |> put_private(:plug_skip_csrf_protection, false)
    |> post(~p"/users/log_in", %{
      "user" => %{
        "_csrf_token" => "invalid",
        "email" => "test@test.com",
        "password" => "Testtesttest123"
      }
    })
  end
end

If you run these tests before you add the defimpl functions to your application, you should see the same exceptions as mentioned above. Now, add the defimpl functions and run the tests again. No more exceptions!

Conclusion

And that’s it! I hope you enjoyed this article! If you want to support me, check out my latest startup Indie Courses or video course. Follow me on BlueSky or subscribe to my newsletter below if you want to get notified when I publish the next blog post. Until the next time! Cheerio 👋

Liked this article? Sign up for more