tjdam

tjdam

"No response body" on particular Stripe webhook events

Hi, all!

I’m Stripe on my project and I setup a webhook endpoint following this guide: https://connerfritz.com/blog/stripe-webhooks-in-phoenix-with-elixir-pattern-matching

It uses stripity-stripe, it’s pretty basic and works well, except it sometimes returns “No response body” on some particular events and I’m not sure why.

At first I had this issue consistently and I realized it was because I wasn’t returning a 200 immediately after the event was received, so I overcame it by triggering a Task to do what I need done and returning a 200 right after it.

So the way my webhook handlers look right now is:

defp handle_webhook(%{type: "some.stripe.event.type"}) do
    IO.puts("Something happened")
    Task.start(fn ->
        # Do something potentially slow
    end)
 
    :ok
end

So when the webhook handler returns that :ok the function that receives all webhooks calls a function that returns a 200 as response:

  defp handle_success(conn) do
    conn
    |> put_resp_content_type("text/plain")
    |> send_resp(200, "ok")
  end

So… am I missing something here? Is there something in this pattern that could potentially slow returning 200 to Stripe?

Thank you :slight_smile:

Most Liked

stefanchrobot

stefanchrobot

When you start a task under a supervisor, you’re getting better observability (via the observer) and proper graceful shutdown - when you’re shutting down your application (e.g. because you’re doing a deployment of a new version), the application will wait until the task completes. The docs for Task.start:

If the current node is shutdown, the node will terminate even if the task was not completed. For this reason, we recommend to use Task.Supervisor.start_child/2 instead, which allows you to control the shutdown time via the :shutdown option.

With just Task.start, the VM shutdown will just kill the task immediately, which means you might “loose” the webhook (Stripe won’t retry as you’ve confirmed it’s been accepted).

Moving to a Task.Supervisor is pretty straightforward:

  • Add {Task.Supervisor, name: MyApp.TaskSupervisor} to your children in MyApp.Application,
  • Start tasks under the supervisor with Task.Supervisor.start_child:
Task.Supervisor.start_child(MyApp.TaskSupervisor, fn ->
  IO.puts "I am running in a task"
end)

start_child seems to be what you need in your use case, but see the docs if you need to await for the result of the task.

The rule of thumb is that no task should run unsupervised (if the task completes before the code that invokes it, you should be fine with using them directly though).

But all of this won’t protect you from the VM being insta-killed from the outside (e.g. OOM killer or a deployment process that doesn’t do proper app termination) - you’re still susceptible to loosing a webhook/task in the middle of processing. The answer to that would be to use persistent background jobs for processing. Oban is an amazing tool for that - you would enqueue a job in the controller (which is really fast) and the processing will happen in the background and you get access to all the features (retries, backoff, concurrency control, monitoring, etc.) as a bonus.

stefanchrobot

stefanchrobot

Enqueueing an Oban job is really fast since it’s just a single insert into your DB, so it can be - and in this case should be - done synchronously. Yes, it introduces a small delay, but you’re winning the consistency here plus I’m sure you’ll still have plenty of time to do some more stuff in the handler if you need to. So it would be something like:

defp handle_webhook(%{type: "some.stripe.event.type"} = event) do
  event
  |> HandleStripeWebhookJob.new()
  # or Oban.insert() if you want explicit error handling, but this is unlikely to fail
  |> Oban.insert!()
end

Oban spawns a pool of workers that will process this job asynchronously in the background.

One of the coolest features of Oban is that it uses your DB and your Ecto repo, so you can insert a job from within a transaction for even more consistency. So you can store the event separately for better inspection:

defp handle_webhook(%{type: "some.stripe.event.type"} = event_data) do
  # returns {:ok, event} or {:error, reason}
  Repo.transaction(fn ->
    with {:ok, event} <- store_event(event_data),
         {:ok, _job} <- enqueue_job(event) do
      # event was stored and the job was enqueued
      # Repo.transaction will wrap "event" in an {:ok, event} tuple
      event
    else
      # something went wrong; discard the event and the job
      # Repo.transaction will wrap the "reason" in {:error, reason} tuple 
      {:error, reason} -> Repo.rollback(reason)
    end
  end)
end

defp store_event(event_data) do
  %{event_data: event_data}
  # assuming we have an Ecto schema named "Event"
  |> Event.create_changeset()
  |> Repo.insert()
end

defp enqueue_job(%Event{} = event) do
  %{event_id: event.id}
  |> HandleStripeWebhookJob.new()
  # Oban.insert is Repo.insert with extra features
  |> Oban.insert()
end
tjdam

tjdam

This is a great answer, thank you so much for this!

I’ve been meaning to look into Oban earlier but I was promising myself not to add unnecessary complexity until I actually needed it, so glad to see that this complexity is not all that complex — at the very basic use case at least :slight_smile:

Where Next?

Popular in Questions Top

nobody
How to bind a phoenix app to a specific ip address? could not find anything about that, nowhere, unfortunately, but for me this is quite...
New
WestKeys
Currently suffering from paralysis by [HTTP client] analysis. This is rather unusual in Elixirland as there tends to be consensus on the ...
New
baxterw3b
Hi guys, i’m new in the Elixir world, and i have to say, that i love it! i’m having some problem to understand anonymous functions with ...
New
greenz1
I have a phoenix application from which a user can download multiple(5-6) files of size 1MB. I couldn’t find anything related to sending ...
New
ovidiubadita
Hey all, I discovered Elixir and I love it. I always wanted to learn a functional programming and I intended to go for Haskell, but afte...
New
JulienCorb
I am trying to implement my new.html.eex file to create new posts on my website. new.html.eex: &lt;h1&gt;Create Post&lt;/h1&gt; &lt;%= ...
New
ycv005
I have followed this StackOverflow post to install the specific version of Erlang. And When I am running mix ecto.setup then getting fol...
New
Qqwy
Original source of discussion: This topic on the Pragmatic Programmers’ Functional Web Development with Elixir, OTP, and Phoenix forum. ...
New
fayddelight
I tried installing elixir 1.11.2 erlang 23.3.4 via asdf in my zsh shell. Enabled the versions locally and globally. When I list them ...
New
rms.mrcs
Hi, I need to transform a list of numbers into a map where the keys are the indexes and the values are the original values of the list. ...
New

Other popular topics Top

New
sorentwo
Hello! tl;dr Announcing Oban, an Ecto based job processing library with a focus on reliability and historical observability. After spen...
985 43487 311
New
pmjoe
I have a relationship of love and hate with Elixir. Lots of things are just absolutely right, but there are some things that are kind of ...
New
jay1
Why is it that the mnesia database isn’t the most preferred database for use in Elixir/Phoenix?
New
vonH
When I run the Plug and I recompile I wind up having to use Ctrl C to quit iex and start again. Witht the help of rlwrap I can use the cu...
New
grych
Hi folks, Few months ago I have announced the proof-of-concept of the library to manipulate the browsers DOM objects directly from Elixi...
639 52673 488
New
bsollish-terakeet
Credo is smart enough to check for (something like) this: assert length(the_list) == 0 with this response: Checking if an enum is empt...
New
jononomo
For some reason my phoenix channels are working for me in my local dev environment, but as soon as I deploy via Docker, I get a 403 error...
New
dokuzbir
I want to highlight html closing tags when i click a html tag. That works in .html files but doesnt work for html.eex templates. How can...
New
svb
Hi! Currently I want to submit a form by pressing the Enter key. However, since my input field is of type “textarea” this is just adds a...
New

Latest on Elixir Forum

Elixir Forum

We're in Beta

About us Mission Statement