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