DEV Community

Arandi López
Arandi López

Posted on

Efficent webhook handling with Ruby on Rails

Originally published in Dev Knights

When implementing a payment system as Paypal or Stripe, the more tedious part is implementing webhooks. In this post I wil explain you a how you can optimize the webhook and get a clean and light implementation.


What are the Webhooks for?

Webhooks are endpoints in your application where a service like Paypal or Stripe will inform you about the events happening in them. Paypal and Stripe use them to notify whether a charge was successful or not, for example. This mechanism is necesary given the charges proccess are asyncronous. Ergo, in the moment we create a charge using thier API we won't know until in the future if the charge was successful or not.

Standard way

The webhook is just an endpoint (route or path) which receives information through POST method. In Rails a barebone implementation of Stripe webhook could be:

# controllers/stripe_controller.rb # Method responsbile for handling stripe webhooks def webhook begin event = JSON.parse(request.body.read) case event['type'] when 'invoice.payment_succeeded' # handle success invoice event when 'invoice.payment_failed' # handle failure invoice event # More events... end rescue Exception => ex render :json => {:status => 400, :error => "Webhook failed"} and return end render :json => {:status => 200} end 
Enter fullscreen mode Exit fullscreen mode

This webhook method uses a switch-case in order to determinate, with the event type that was received, which code block execute. Now imagine implement more events, this method will smell and get hard to maintain.

Sure we can write more methods to put the logic of each event and call them into the switch-case, however we can still improve this webhook method.

Cleaner way

The cleanest webhook implementation I've seen is in Laravel Cashier. Cashier's Webhook Controller allows you to handle automaticaly by mapping each event to a method. The controller convention says that if you want to handle the event invoice.payment_succeeded create a method in the controller whose name start with handle and the camelCase form of the event type: handleInvoicePaymentSucceeded.

<?php namespace App\Http\Controllers; use Laravel\Cashier\Http\Controllers\WebhookController as CashierController; class WebhookController extends CashierController { /** * Handle a Stripe webhook. * * @param array $payload * @return Response */ public function handleInvoicePaymentSucceeded($payload) { // Handle The Event } } 
Enter fullscreen mode Exit fullscreen mode

Refactoring

How can we achieve this with Rails?. Easy, thanks to metaprogramming in Ruby. Refactoring the previous method could get like this:

def webhook begin event = JSON.parse(request.body.read) method = "handle_" + event['type'].tr('.', '_') self.send method, event rescue JSON::ParserError => e render json: {:status => 400, :error => "Invalid payload"} and return rescue NoMethodError => e # missing event handler end render json: {:status => 200} end def handle_invoice_payment_succeeded(event) #handle the event end def handle_invoice_payment_failed(event) #handle the event end # and more... 
Enter fullscreen mode Exit fullscreen mode

In the webhook method we need to parse the event type to the name of the method which will handle that event.

  1. Translate all '.' to '_' and concatenate with "handle_".
  2. Then send the message to call the resultant method with the payload obtained.
  3. Otherwise, if there's no method implemented for that event, it will be rescued where we can make another action.

Laravel Cashier implementation looks wonderful to handle webhook events. Therefore this way you get a clean and maintainable controller.

ProTip

As a next step you could write concerns to group event handlers. Now you have a lighter controller.

Do you want to read this article in spanish? Check it here

Top comments (6)

Collapse
 
pcreux profile image
Philippe Creux

This is better indeed.

Next level is to queue up jobs to process each webhook. You could exhaust the http connection pool if you get hammered with webhooks. Also, Stripe webhooks timeout (and retry!) if it takes a couple of seconds to process them.

I tend to persist the webhook payload in the DB and process it async. It makes debugging and replaying way easier.

Thank you for sharing!

Collapse
 
dmulter profile image
David Multer

Good stuff, but there are definitely lots of challenges with webhooks like what Philippe points out, signature verification, and more. They are always easy to get started with, but plenty of work lay ahead.

Collapse
 
amit_savani profile image
Amit Patel

I would prefer to use separate service object to handle each event over instance methods within the webhook handler.

Collapse
 
mihaibb profile image
Mihai

Here's another approach that you might want to consider:

# controllers/webhooks/stripe_controller.rb class Webhooks::StripeController < ApplicationController before_action :authorize_request! def handler payload = request.request_parameters # this will parse the body to json if content type is json # 1. Save the event and process in background save_event_for_the_record_and_process_later # 2. Process now process_now(payload) end private # Save events in db or other system so you can process event in background # and also to have a record of what you actually got from stripe. # It might be helful to debug future issues. def save_event_for_the_record(payload) event = Webhooks::StripeEvent.new(payload: payload) if event.save event.process_later head(:ok) else head :unprocessable_entity end end def process_now(payload) case payload['event'] when .... end end def authorize_request! # validate request ... end end 
Collapse
 
madeindjs profile image
Alexandre Rousseau

I think this refactoring is dangerous because you allow code injection. You should verify event['type'] content before call self.send

Collapse
 
arandilopez profile image
Arandi López

Sure, I need to implement Signature check