Current section

Files

Jump to
stripity_stripe lib stripe webhook_plug.ex
Raw

lib/stripe/webhook_plug.ex

if Code.ensure_loaded?(Plug) do
defmodule Stripe.WebhookPlug do
@moduledoc """
Helper `Plug` to process webhook events and send them to a custom handler.
## Installation
To handle webhook events, you must first configure your application's endpoint.
Add the following to `endpoint.ex`, **before** `Plug.Parsers` is loaded.
```elixir
plug Stripe.WebhookPlug,
at: "/webhook/stripe",
handler: MyAppWeb.StripeHandler,
secret: "whsec_******"
```
If you have not yet added a webhook to your Stripe account, you can do so
by visiting `Developers > Webhooks` in the Stripe dashboard. Use the route
you configured in the endpoint above and copy the webhook secret into your
app's configuration.
### Supported options
- `at`: The URL path your application should listen for Stripe webhooks on.
Configure this to match whatever you set in the webhook.
- `handler`: Custom event handler module that accepts `Stripe.Event` structs
and processes them within your application. You must create this module.
- `secret`: Webhook secret starting with `whsec_` obtained from the Stripe
dashboard. This can also be a function or a tuple for runtime configuration.
- `tolerance`: Maximum age (in seconds) allowed for the webhook event.
See `Stripe.Webhook.construct_event/4` for more information.
## Handling events
You will need to create a custom event handler module to handle events.
Your event handler module should implement the `Stripe.WebhookHandler`
behavior, defining a `handle_event/1` function which takes a `Stripe.Event`
struct and returns either `{:ok, term}` or `:ok`. This will mark the event as
successfully processed. Alternatively handler can signal an error by returning
`:error` or `{:error, reason}` tuple, where reason is an atom or a string.
HTTP status code 400 will be used for errors.
### Example
```elixir
# lib/myapp_web/stripe_handler.ex
defmodule MyAppWeb.StripeHandler do
@behaviour Stripe.WebhookHandler
@impl true
def handle_event(%Stripe.Event{type: "charge.succeeded"} = event) do
# TODO: handle the charge.succeeded event
end
@impl true
def handle_event(%Stripe.Event{type: "invoice.payment_failed"} = event) do
# TODO: handle the invoice.payment_failed event
end
# Return HTTP 200 for unhandled events
@impl true
def handle_event(_event), do: :ok
end
```
## Configuration
You can configure the webhook secret in your app's own config file.
For example:
```elixir
config :myapp,
# [...]
stripe_webhook_secret: "whsec_******"
```
You may then include the secret in your endpoint:
```elixir
plug Stripe.WebhookPlug,
at: "/webhook/stripe",
handler: MyAppWeb.StripeHandler,
secret: Application.get_env(:myapp, :stripe_webhook_secret)
```
### Runtime configuration
If you're loading config dynamically at runtime (eg with `runtime.exs`
or an OTP app) you must pass a tuple or function as the secret.
```elixir
# With a tuple
plug Stripe.WebhookPlug,
at: "/webhook/stripe",
handler: MyAppWeb.StripeHandler,
secret: {Application, :get_env, [:myapp, :stripe_webhook_secret]}
# Or, with a function
plug Stripe.WebhookPlug,
at: "/webhook/stripe",
handler: MyAppWeb.StripeHandler,
secret: &MyAppWeb.Secrets.stripe_webhook_secret/0 # a remote function in the format &Mod.fun/arity
```
"""
@behaviour Plug
import Plug.Conn
alias Plug.Conn
@impl Plug
def init(opts) do
path_info = String.split(opts[:at], "/", trim: true)
opts
|> Enum.into(%{})
|> Map.put_new(:path_info, path_info)
end
@impl Plug
def call(
%Conn{method: "POST", path_info: path_info} = conn,
%{
path_info: path_info,
secret: secret,
handler: handler
} = opts
) do
secret = parse_secret!(secret)
with [signature] <- get_req_header(conn, "stripe-signature"),
{:ok, payload, conn} <- Conn.read_body(conn) do
process_event(conn, payload, signature, secret, handler, opts)
else
_ -> halt(send_resp(conn, 400, "Bad request."))
end
end
@impl Plug
def call(%Conn{path_info: path_info} = conn, %{path_info: path_info}) do
halt(send_resp(conn, 400, "Bad request."))
end
@impl Plug
def call(conn, _), do: conn
# Body has been read; `conn` here is the post-`read_body` conn passed as a
# parameter, so it stays in scope for both the success and error branches.
# Sending the response on the pre-`read_body` conn confuses Bandit's
# connection state on keep-alive HTTP/1.1 and can corrupt the next request.
defp process_event(conn, payload, signature, secret, handler, opts) do
with {:ok, %Stripe.Event{} = event} <- construct_event(payload, signature, secret, opts),
:ok <- handle_event!(handler, event) do
halt(send_resp(conn, 200, "Webhook received."))
else
{:handle_error, reason} -> halt(send_resp(conn, 400, reason))
_ -> halt(send_resp(conn, 400, "Bad request."))
end
end
defp construct_event(payload, signature, secret, %{tolerance: tolerance}) do
Stripe.Webhook.construct_event(payload, signature, secret, tolerance)
end
defp construct_event(payload, signature, secret, _opts) do
Stripe.Webhook.construct_event(payload, signature, secret)
end
defp handle_event!(handler, %Stripe.Event{} = event) do
telemetry_meta = %{event: event.type, handler_status: nil}
:telemetry.span(~w[stripe webhook]a, telemetry_meta, fn ->
case handler.handle_event(event) do
{:ok, _} ->
{:ok, %{telemetry_meta | handler_status: :ok}}
:ok ->
{:ok, %{telemetry_meta | handler_status: :ok}}
{:error, reason} when is_binary(reason) ->
{{:handle_error, reason}, %{telemetry_meta | handler_status: :error}}
{:error, reason} when is_atom(reason) ->
{{:handle_error, Atom.to_string(reason)}, %{telemetry_meta | handler_status: :error}}
:error ->
{{:handle_error, ""}, %{telemetry_meta | handler_status: :error}}
resp ->
raise """
#{inspect(handler)}.handle_event/1 returned an invalid response. Expected {:ok, term}, :ok, {:error, reason} or :error
Got: #{inspect(resp)}
Event data: #{inspect(event)}
"""
end
end)
end
defp parse_secret!({m, f, a}), do: apply(m, f, a)
defp parse_secret!(fun) when is_function(fun), do: fun.()
defp parse_secret!(secret) when is_binary(secret), do: secret
defp parse_secret!(secret) do
raise """
The Stripe webhook secret is invalid. Expected a string, tuple, or function.
Got: #{inspect(secret)}
If you're setting the secret at runtime, you need to pass a tuple or function.
For example:
plug Stripe.WebhookPlug,
at: "/webhook/stripe",
handler: MyAppWeb.StripeHandler,
secret: {Application, :get_env, [:myapp, :stripe_webhook_secret]}
"""
end
end
end