Skip to content

Commit

Permalink
Allow errors to be returned by webhook handler (#712)
Browse files Browse the repository at this point in the history
* Allow webhook event handler to return an error state

* Update webhook plug tests with :error events
  • Loading branch information
deniskulicek authored Feb 15, 2022
1 parent 3a917f3 commit 78fb916
Show file tree
Hide file tree
Showing 3 changed files with 88 additions and 7 deletions.
4 changes: 3 additions & 1 deletion lib/stripe/webhook_handler.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
17 changes: 15 additions & 2 deletions lib/stripe/webhook_plug.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)}
Expand Down
74 changes: 70 additions & 4 deletions test/stripe/webhook_plug_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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(
Expand Down

0 comments on commit 78fb916

Please sign in to comment.