Handle Phoenix Exceptions gracefully
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!
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 👋