I am trying to write a plug which will generate a custom error if the request has malformed JSON which is quite often the case in our scenarios(as we use variables in postman. eg sometimes there is no quote outside the value and it results in malformed JSON). The only help I got is https://groups.google.com/forum/#!topic/phoenix-talk/8F6upFh_lhc which isnt working of course.
defmodule PogioApi.Plug.PrepareParse do
import Plug.Conn
@env Application.get_env(:application_api, :env)
def init(opts) do
opts
end
def call(conn, opts) do
%{method: method} = conn
# TODO: check for PUT aswell
if method in ["POST"] and not(@env in [:test]) do
{:ok, body, _conn} = Plug.Conn.read_body(conn)
case Jason.decode(body) do
{:ok, _result} -> conn
{:error, _reason} ->
error = %{message: "Malformed JSON in the body"}
conn
|> put_resp_header("content-type", "application/json; charset=utf-8")
|> send_resp(400, Jason.encode!(error))
|> halt
end
else
conn
end
end
end
This line
{:ok, body, _conn} = Plug.Conn.read_body(conn)
How to read and parse body properly. I know in POST, we will always get format=JSON request
Issue: issue is body can be read only once. Plug.Parses wont be able to find body if I read it before in my custom plug
in endpoint.ex file add a custom body reader and your custom plug in below order
plug Api.Plug.PrepareParse # should be called before Plug.Parsers
plug Plug.Parsers,
parsers: [:urlencoded, :multipart, :json],
pass: ["*/*"],
body_reader: {CacheBodyReader, :read_body, []}, # CacheBodyReader option is also needed
json_decoder: Phoenix.json_library()
Define a custom body reader
defmodule CacheBodyReader do
def read_body(conn, _opts) do
# Actual implementation
# {:ok, body, conn} = Plug.Conn.read_body(conn, opts)
# conn = update_in(conn.assigns[:raw_body], &[body | (&1 || [])])
# {:ok, body, conn}
{:ok, conn.assigns.raw_body, conn}
end
end
Then your custom parse prepare
defmodule Api.Plug.PrepareParse do
import Plug.Conn
@env Application.get_env(:application_api, :env)
@methods ~w(POST PUT PATCH PUT)
def init(opts) do
opts
end
def call(conn, opts) do
%{method: method} = conn
if method in @methods and not (@env in [:test]) do
case Plug.Conn.read_body(conn, opts) do
{:error, :timeout} ->
raise Plug.TimeoutError
{:error, _} ->
raise Plug.BadRequestError
{:more, _, conn} ->
# raise Plug.PayloadTooLargeError, conn: conn, router: __MODULE__
error = %{message: "Payload too large error"}
render_error(conn, error)
{:ok, "" = body, conn} ->
body = "{}" // otherwise error
update_in(conn.assigns[:raw_body], &[body | &1 || []])
{:ok, body, conn} ->
case Jason.decode(body) do
{:ok, _result} ->
update_in(conn.assigns[:raw_body], &[body | &1 || []])
{:error, _reason} ->
error = %{message: "Malformed JSON in the body"}
render_error(conn, error)
end
end
else
conn
end
end
def render_error(conn, error) do
conn
|> put_resp_header("content-type", "application/json; charset=utf-8")
|> send_resp(400, Jason.encode!(error))
|> halt
end
end
Few References: