Write Malformed JSON in the body Plug

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

How to read and parse body properly. I know in POST, we will always get format=JSON request

That will read the body, if it hasn’t been read before, eg by Plug.Parsers.

After you have read the body, you should put the parsed data somewhere in the conn, or it will be lost.

1 Like

my current state of plug

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
    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} ->
          private = Map.put(conn.private, :body, body)
          %{conn | private: private}
        {: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

I am trying to put back the body I read but not successful. Reference: How do you put a request body in a Plug.Conn?

I am calling parser after my custom plug. So I need to put back body which I have read so that nothing else got effected

What do you mean by “not successful”?

Do you get an error? Is your plug not called at all? Can you give a repo with a minified, but mix phx.serverable project?

means request body is not passed to Plug.Parsers as its already been read by my custom plug

Maybe this will clear my question a little bit more

Correct. Thats impossible.

There can only be one plug that calls Plug.Conn.read_body/2:

Because the request body can be of any size, reading the body will only work once, as Plug will not cache the result of these operations. If you need to access the body multiple times, it is your responsibility to store it. Finally keep in mind some plugs like Plug.Parsers may read the body, so the body may be unavailable after being accessed by such plugs.

can we do this? How do you tell Plug.Parsers to read body from conn.assigns[:req_body]

if there is a way to override Plug.Parsers.ParseError that will solve my issue too

Maybe you can use a custom body reader: https://hexdocs.pm/plug/1.10.0/Plug.Parsers.html#module-custom-body-reader

I got it working with custom body reader. Thank you

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)

  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} ->
          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
1 Like