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