I know this thread is pretty old, but it’s high up in the search function. I managed to find a semi-elegant solution for this.
I made a custom plug to call out to Plug.Parsers and handle any exceptions it throws.
defmodule DataApiWeb.Plugs.QuietJSON do
@behaviour Plug
import Plug.Conn
@impl true
def init(opts) do
# Create the optoins that `Plug.Parsers` expects.
Plug.Parsers.init(opts)
end
@impl true
def call(conn, opts) do
Plug.Parsers.call(conn, opts)
rescue
Plug.Parsers.ParseError ->
conn
|> put_resp_content_type("application/json")
|> send_resp(400, ~s({"error":{"detail":"Malformed JSON"}}))
|> halt()
end
end
Then, in my endpoint.ex I’ve made this change:
# plug Plug.Parsers,
plug DataApiWeb.Plugs.QuietJSON,
parsers: [:urlencoded, :multipart, :json],
pass: ["*/*"],
json_decoder: Phoenix.json_library()
This approach simply takes in the options from the original plug and passes them on to the wrapped plug.
On malformed data, the plug returns the error message {"error": {"detail": "Malformed JSON"}} and you shouldnt see any error logs anymore.
A sample test I added to verify the behavior:
test "invalid JSON body returns a clean 400", %{conn: conn} do
conn =
conn
|> put_req_header("content-type", "application/json")
|> post(~p"/api/health", "{not-json")
assert %{"error" => _} = json_response(conn, 400)
end
The reason I needed this was because some bots were spamming and it polluted my log files.






















