Vinicius Brasil

May 27, 2022

Read raw body from Plug.Conn after parsers

If you ever tried to use Plug.Conn.read_body/2 and it returned nil, you now know that it can only be used once. If you have :json in your Plug.Parsers, you cannot read the raw body again, because it was already used by the parser. That’s a trick Plug does to save memory as request bodies can be of any size.

However, if you’re dealing with external webhooks, you may need both the raw body and the parsed map. There is an option on Plug.Parsers that lets you cache the raw body before it’s read by parsers called :body_reader. Please note that by doing this you’ll have an extra load of memory, as you’ll now have map and string representations of the request body in %Plug.Conn{}.

To start off we’ll need to create a module to be passed on to the :body_readeroption. This module must have a read_body/2 function that stores the raw string body in Plug’s private storage.

defmodule MyApp.CacheBodyReader do
  @moduledoc false

  @raw_body_key :my_app_raw_body

  def read_body(conn, opts \\ []) do
    case Plug.Conn.read_body(conn, opts) do
      {:ok, binary, conn} -> {:ok, binary, append_chunk(conn, binary)}
      {:more, binary, conn} -> {:more, binary, append_chunk(conn, binary)}
      {:error, reason} -> {:error, reason}
    end
  end

  defp append_chunk(conn, chunk) do
    existing = conn.private[@raw_body_key] || []
    Plug.Conn.put_private(conn, @raw_body_key, [existing | chunks])
  end

  def get_raw_body(conn) do
    case conn.private[@raw_body_key] do
      nil -> nil
      chunks -> chunks |> Enum.reverse() |> Enum.join("")
    end
  end
end

The read_body/2 function is the main one. It reads the body using Plug.Conn.read_body/2, which returns either the whole body or the body in chunks, depending on size. Either way, a list of chunks is stored using Plug.Conn.put_private/3.

Just add MyApp.CacheBodyReader to your Plug.Parsers call, and you’re done.

defmodule MyApp.Endpoint do
  # ...

  plug(Plug.Parsers,
    parsers: [:json],
    json_decoder: Jason,
    body_reader: {MyApp.CacheBodyReader, :read_body, []}
  )
end

Whenever you need to use the raw body in a controller, just call MyApp.CacheBodyReader.get_raw_body/1, and it’ll read from the private storage in memory, joining the string body chunks into a single string.

defmodule MyApp.Controller do
  # ...

  def call(conn, _opts) do
    MyApp.CacheBodyReader.get_raw_body(conn)
    # "{\"hello\":\"world\"}"
  end
end