PhoenixMountable.RawBody (PhoenixMountable v0.2.4)

Copy Markdown View Source

The body of a request as it was sent, for the routes that check a signature of it: a mail provider's webhooks, GitHub's, Stripe's.

In a Phoenix app the endpoint's Plug.Parsers reads the body before the router, and a body can be read once. A Rails engine's controller can still read it (Rack keeps it rewindable); a library's route in Phoenix needs the endpoint to keep a copy. One reader keeps it for every library, given to Plug.Parsers with the app's router:

plug Plug.Parsers,
  parsers: [:urlencoded, :multipart, :json],
  pass: ["*/*"],
  body_reader: {PhoenixMountable.RawBody, :read_body, [MyAppWeb.Router]},
  json_decoder: Phoenix.json_library()

use PhoenixMountable.Install, raw_body: true adds it to the app's endpoint.

Which requests

Before it reads a body, the reader asks the router which plug the request will reach (Phoenix.Router.route_info/4, through the routers it forwards to), and keeps a copy only when that plug says it needs the body as sent:

defmodule MyLib.Webhook do
  use PhoenixMountable.RawBody
  # every route to this plug
end

defmodule MyLibWeb.HookController do
  use MyLibWeb, :controller
  use PhoenixMountable.RawBody, only: [:create]
  # the routes to these actions
end

So nothing depends on a route's path, scope or pipeline: the app can move and rename the routes, and their bodies are kept. Every other request is read as Plug.Conn.read_body/2 reads it, with one lookup in the router's compiled routes the first time, and nothing kept.

The router can be a list of routers (an app with more than one endpoint's worth, or a router no forward reaches).

Reading it

In the plug, fetch/2 gives the body: the copy the reader kept, or the body itself when the parsers left it unread (a type they pass, such as a raw email's message/rfc822).

{:ok, body, conn} = PhoenixMountable.RawBody.fetch(conn, 1_000_000)

Summary

Functions

Marks a plug, or a controller's actions (only:), as needing the body of their requests as it was sent.

The body of the request as it was sent, at most max bytes: the copy read_body/3 kept, or the body read now when the parsers left it unread. {:error, :too_large} past max, and {:error, :no_raw_body} when the parsers read it without keeping it (the endpoint has not got the reader, or the route's plug does not use PhoenixMountable.RawBody).

A body reader for Plug.Parsers (body_reader: {PhoenixMountable.RawBody, :read_body, [router]}): reads the body as Plug.Conn.read_body/2 does, and keeps a copy of it for the routes that want it.

Whether the request to path_info reaches a plug that wants its body as sent, through the routers router forwards to.

Functions

__using__(opts)

(macro)

Marks a plug, or a controller's actions (only:), as needing the body of their requests as it was sent.

fetch(conn, max)

@spec fetch(Plug.Conn.t(), pos_integer()) ::
  {:ok, binary(), Plug.Conn.t()} | {:error, :too_large | :no_raw_body | term()}

The body of the request as it was sent, at most max bytes: the copy read_body/3 kept, or the body read now when the parsers left it unread. {:error, :too_large} past max, and {:error, :no_raw_body} when the parsers read it without keeping it (the endpoint has not got the reader, or the route's plug does not use PhoenixMountable.RawBody).

read_body(conn, opts, router)

@spec read_body(Plug.Conn.t(), keyword(), module() | [module()]) ::
  {:ok, binary(), Plug.Conn.t()}
  | {:more, binary(), Plug.Conn.t()}
  | {:error, term()}

A body reader for Plug.Parsers (body_reader: {PhoenixMountable.RawBody, :read_body, [router]}): reads the body as Plug.Conn.read_body/2 does, and keeps a copy of it for the routes that want it.

wanted?(router, method, path_info, host)

@spec wanted?(module(), String.t(), [String.t()], String.t()) :: boolean()

Whether the request to path_info reaches a plug that wants its body as sent, through the routers router forwards to.