towerops/lib/towerops_web/plug_exceptions.ex

69 lines
2.3 KiB
Elixir

defmodule ToweropsWeb.PlugExceptions do
@moduledoc """
Protocol implementations for converting expected Phoenix exceptions
into clean HTTP status codes rather than raising errors.
This reduces noise in error tracking by treating invalid requests
(wrong MIME types, invalid CSRF tokens) as bad requests rather than
application failures.
## Rationale
Production applications receive noise from legitimate bad requests:
- API clients sending incorrect Accept headers
- Mobile apps with stale CSRF tokens after backgrounding
- Automation tools (Terraform, scripts) with malformed requests
These are client errors (400-level), not server failures (500-level).
By implementing `Plug.Exception` protocol for these exceptions, we:
1. Return clean HTTP 400 Bad Request responses
2. Reduce exception tracking alerts for expected client errors
3. Keep error logs focused on genuine application failures
## Implementation
Each exception type gets a protocol implementation with two callbacks:
- `status/1` - Returns the HTTP status code (400 for bad requests)
- `actions/1` - Returns suggested actions (empty list for now)
## Covered Exception Types
- `Phoenix.NotAcceptableError` - Invalid Accept headers from API clients
- `Plug.CSRFProtection.InvalidCSRFTokenError` - Stale or invalid CSRF tokens
## Monitoring
These exceptions are still logged at debug level, allowing monitoring
without triggering production alerts. If you see patterns indicating
attacks (e.g., excessive CSRF failures), investigate via logs.
## See Also
- Documentation: `docs/exception_handling.md`
- Tests: `test/towerops_web/plug_exceptions_test.exs`
"""
require Logger
# Invalid Accept headers from API consumers (automation, mobile apps, etc.)
defimpl Plug.Exception, for: Phoenix.NotAcceptableError do
def status(exception) do
Logger.debug("Converting Phoenix.NotAcceptableError to 400: #{inspect(exception)}")
400
end
def actions(_exception), do: []
end
# Stale CSRF tokens from mobile app or browser sessions
defimpl Plug.Exception, for: Plug.CSRFProtection.InvalidCSRFTokenError do
def status(exception) do
Logger.debug("Converting InvalidCSRFTokenError to 400: #{inspect(exception)}")
400
end
def actions(_exception), do: []
end
end