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: Stripe Webhooks in Phoenix with Elixir Pattern Matching | Conner Fritz

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:

stefanchrobot

stefanchrobot

From the code you’ve shown, it looks OK. Although you should probably follow the docs and run your tasks under a supervisor, or better yet use something like Oban so as not to loose anything.

Where Next?

Popular in Questions Top

dotdotdotPaul
Okay, I'm having a heck of a time trying to figure out how to best handle the validation of belongs_to associations in Ecto. I'm sure I'...
New
LegitStack
I’m hoping you guys can give me some general advice and perhaps code examples if you’re feeling up to it. I’m very interested in Elixir,...
New
New
dokuzbir
Hello, I am trying to convert my lists to string without losing brackets.For start i have 3 map. They look like these buyer = %{ id: ...
New
vac
Hi, I'm quite new in Elixir and I'm trying to format a string to a PEM format. I have the certificate value like MIIDBTCCAe2...... and ...
New
chensan
I have a User schema with a :from_id field set to type :string: defmodule TweetBot.Repo.Migrations.CreateUsers do use Ecto.Migration ...
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
Patoshizzle
After calling mix ecto.create I get this error: 17:00:32.162 [error] GenServer #PID&lt;0.412.0&gt; terminating ** (Postgrex.Error) FATAL...
New
romenigld
I am trying to run a deploy with docker and I successfully runned with this command: docker build -t romenigld/blog-prod . but when I t...
New
vrod
I am using the Starship cross-shell prompt – it seems pretty nice, but I get some errors: [WARN] - (starship::utils): Executing command ...
New

Other popular topics Top

lessless
I believe there are people here who are dealing with CSV files import on the daily basis, and since Excel is a really popular tool there ...
New
sergio_101
I am VERY much an elixir newbie. I have taken one elixir course and one phoenix course on Udemy. During that course, I saw the instructor...
New
_russellb
I want to try my hand at web scraping. What tools/libraries do I need to use. I’m hoping to turn this into something professional so don’...
New
New
script
If I have a string “1000 cfu/ml” . I want to remove the characters and / and space . So the string is like this "1000" What is the ...
New
shahryarjb
Hello, I have map which I want to convert it to string like this: the map: %{last_name: "tavakkoli", name: "shahryar"} the string I ne...
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
chrisalley
ExUnit now has describe blocks which is a welcome addition coming from RSpec. In the docs, it states that nested hierarchies of describe ...
New
Fl4m3Ph03n1x
About me? ( if you have nothing better to do than reading about some random guy in the internet :stuck_out_tongue: ) Hello all, this is ...
New
siddhant3030
Hi, I have to write a raw query for one of my project. But till now I have used ecto queries and don’t have much experience writing raw ...
New

We're in Beta

About us Mission Statement