Current section
Files
Jump to
Current section
Files
lib/accrue/webhook/caching_body_reader.ex
defmodule Accrue.Webhook.CachingBodyReader do
@moduledoc """
Custom body reader for `Plug.Parsers` that tees the raw body into
`conn.assigns[:raw_body]` for webhook signature verification.
Only used inside the `:accrue_webhook_raw_body` pipeline (D2-20) --
never globally. This ensures that non-webhook routes are unaffected
by raw-body capture (WH-01).
## Usage
pipeline :accrue_webhook_raw_body do
plug Plug.Parsers,
parsers: [:json],
pass: ["*/*"],
json_decoder: Jason,
body_reader: {Accrue.Webhook.CachingBodyReader, :read_body, []},
length: 1_000_000
end
## Implementation
Chunks are prepended (O(1) cons) during streaming reads and reversed
at flatten time in the consuming plug. This avoids quadratic binary
concatenation when the body arrives in multiple chunks.
"""
@doc """
Reads the request body and tees it into `conn.assigns[:raw_body]`.
Returns `{:ok, body, conn}`, `{:more, body, conn}`, or `{:error, reason}`
matching the `Plug.Conn.read_body/2` contract expected by `Plug.Parsers`.
"""
@spec read_body(Plug.Conn.t(), keyword()) ::
{:ok, binary(), Plug.Conn.t()}
| {:more, binary(), Plug.Conn.t()}
| {:error, term()}
def read_body(conn, opts) do
case Plug.Conn.read_body(conn, opts) do
{:ok, body, conn} ->
conn = update_in(conn.assigns[:raw_body], &[body | &1 || []])
{:ok, body, conn}
{:more, body, conn} ->
conn = update_in(conn.assigns[:raw_body], &[body | &1 || []])
{:more, body, conn}
{:error, reason} ->
{:error, reason}
end
end
end