diff --git a/lib/stripe/webhook_handler.ex b/lib/stripe/webhook_handler.ex index c9c5f52e..ba19ec28 100644 --- a/lib/stripe/webhook_handler.ex +++ b/lib/stripe/webhook_handler.ex @@ -3,7 +3,9 @@ defmodule Stripe.WebhookHandler do Webhook handler specification. See `Stripe.WebhookPlug` for more details. """ + @type error_reason :: binary() | atom() @doc "Handles a Stripe webhook event within your application." - @callback handle_event(event :: Stripe.Event.t()) :: {:ok, term} | :ok + @callback handle_event(event :: Stripe.Event.t()) :: + {:ok, term} | :ok | {:error, error_reason} | :error end diff --git a/lib/stripe/webhook_plug.ex b/lib/stripe/webhook_plug.ex index 799036b4..50fd04a4 100644 --- a/lib/stripe/webhook_plug.ex +++ b/lib/stripe/webhook_plug.ex @@ -36,7 +36,10 @@ defmodule Stripe.WebhookPlug do 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`. + 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 @@ -133,6 +136,7 @@ defmodule Stripe.WebhookPlug do :ok <- handle_event!(handler, event) do send_resp(conn, 200, "Webhook received.") |> halt() else + {:handle_error, reason} -> send_resp(conn, 400, reason) |> halt() _ -> send_resp(conn, 400, "Bad request.") |> halt() end end @@ -161,9 +165,18 @@ defmodule Stripe.WebhookPlug do :ok -> :ok + {:error, reason} when is_binary(reason) -> + {:handle_error, reason} + + {:error, reason} when is_atom(reason) -> + {:handle_error, Atom.to_string(reason)} + + :error -> + {:handle_error, ""} + resp -> raise """ - #{inspect(handler)}.handle_event/1 returned an invalid response. Expected {:ok, term} or :ok + #{inspect(handler)}.handle_event/1 returned an invalid response. Expected {:ok, term}, :ok, {:error, reason} or :error Got: #{inspect(resp)} Event data: #{inspect(event)} diff --git a/test/stripe/webhook_plug_test.exs b/test/stripe/webhook_plug_test.exs index 05c3ee9d..a1463e7d 100644 --- a/test/stripe/webhook_plug_test.exs +++ b/test/stripe/webhook_plug_test.exs @@ -19,6 +19,27 @@ defmodule Stripe.WebhookPlugTest do def handle_event(%Stripe.Event{object: "event"}), do: :ok end + defmodule ErrorTupleStringHandler do + @behaviour Stripe.WebhookHandler + + @impl true + def handle_event(%Stripe.Event{object: "event"}), do: {:error, "string error message"} + end + + defmodule ErrorTupleAtomHandler do + @behaviour Stripe.WebhookHandler + + @impl true + def handle_event(%Stripe.Event{object: "event"}), do: {:error, :atom_error_message} + end + + defmodule ErrorAtomHandler do + @behaviour Stripe.WebhookHandler + + @impl true + def handle_event(%Stripe.Event{object: "event"}), do: :error + end + defmodule BadHandler do def handle_event(_), do: nil end @@ -29,10 +50,11 @@ defmodule Stripe.WebhookPlugTest do timestamp = System.system_time(:second) # TODO: remove when we require OTP 22 - code = case System.otp_release() >= "22" do - true -> :crypto.mac(:hmac, :sha256, @secret, "#{timestamp}.#{payload}") - false -> :crypto.mac(:sha256, @secret, "#{timestamp}.#{payload}") - end + code = + case System.otp_release() >= "22" do + true -> :crypto.mac(:hmac, :sha256, @secret, "#{timestamp}.#{payload}") + false -> :crypto.mac(:sha256, @secret, "#{timestamp}.#{payload}") + end signature = code @@ -107,6 +129,50 @@ defmodule Stripe.WebhookPlugTest do assert result.status == 200 end + test "returns 400 status code with string message if handler returns error tuple", %{ + conn: conn + } do + opts = + WebhookPlug.init( + at: "/webhook/stripe", + handler: __MODULE__.ErrorTupleStringHandler, + secret: @secret + ) + + result = WebhookPlug.call(conn, opts) + assert result.state == :sent + assert result.status == 400 + assert result.resp_body == "string error message" + end + + test "returns 400 status code with atom message if handler returns error tuple", %{conn: conn} do + opts = + WebhookPlug.init( + at: "/webhook/stripe", + handler: __MODULE__.ErrorTupleAtomHandler, + secret: @secret + ) + + result = WebhookPlug.call(conn, opts) + assert result.state == :sent + assert result.status == 400 + assert result.resp_body == "atom_error_message" + end + + test "returns 400 status code with no message if handler returns :error atom", %{conn: conn} do + opts = + WebhookPlug.init( + at: "/webhook/stripe", + handler: __MODULE__.ErrorAtomHandler, + secret: @secret + ) + + result = WebhookPlug.call(conn, opts) + assert result.state == :sent + assert result.status == 400 + assert result.resp_body == "" + end + test "crash hard if handler fails", %{conn: conn} do opts = WebhookPlug.init(