jsonparsingelixirphoenix-frameworkplug

Write custom Plug which can handle and return proper error on malformed JSON in the body


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


Solution

  • 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