3

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

Tanweer
  • 567
  • 1
  • 5
  • 17

1 Answers1

4

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:

  1. https://elixirforum.com/t/how-to-read-request-body-multiple-times-during-request-handling/3845
  2. https://elixirforum.com/t/how-do-you-put-a-request-body-in-a-plug-conn/8584
  3. https://elixirforum.com/t/write-malformed-json-in-the-body-plug/30578
Tanweer
  • 567
  • 1
  • 5
  • 17