DEV Community

Lucas Barret
Lucas Barret

Posted on • Updated on

Webhook backend in ruby

I have heard of Webhook for a while now. But I never used it, and I have struggled a bit to understand what was really happening.

And as always, I think trying to implement the thing is one of the best ways to understand what is happening. So, after reading one book or two about Webhooks and API. I thought it was the time to implement a simple Webhook API.

In this blog post, we will oversee all the non-functional and security parts. We will aim to understand how Webhooks are working and try to understand the spirit of it. But of course, these are important parts that need to be treated in production.

Webhook

Webhook needs several things but let's try to put it simply.
Of course we need two things :

  • A Webhook API provider
  • A Webhook API consumer

The Webhook consumer needs to register with the provider and give a URL that can be requested when an event has happened.

This URL is the callback URL, and it is an endpoint for the client to react to any change happening in the provider system.

Then, we will need an Event history to be able to keep the client up to date on the event if any issue happens.

Eventually, the Webhook consumer will need a callback endpoint to receive any updates.

Provider

Let's say we have a job that launches the washing for our laundry machine. We want to provide a way to make the client aware of when the machine will be ended without polling.

We will get back to this job later for now we only need to know that it is called WashingJob.

First, as we said, the API Consumer needs to register with the API provider. So, the provider needs a Webhook subscription endpoint.

Let's have a look at the WebHook subscription model.
This will take a receiver_url (that will be query when the job is over), the customer_id, and the event that has happened that triggers our webhook (here the end of the washing machine).

class WebHookSubController < ApplicationController
  def index
  end

  def new
  end

  def create

    WebHookSubscription.create(receiver_url: params[:receiver_url],topic: params[:topic], customer_id: params[:customer_id])
  end
end
Enter fullscreen mode Exit fullscreen mode

The next step for us is to have an event history in the API provider to know if the clients have received the event well.

This will be needed in the case that API Consumer has not acknowledged the reception of the new event. So they can get the history and update every event that has not been received on their side. This can happen in different cases. Downtime of the API Receiver endpoint or server for example.

class CreateEventHistory < ActiveRecord::Migration[7.1]
  def change
    create_table :event_histories do |t|
      t.string :topic
      t.string :delivery_status
      t.string :customer_id

      t.timestamps
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

Now we can take a look to the washing job. This will be pretty simple.

So, as you see below, we retrieved the customer subscription to this event. Then we create a row in our history so the customers can have a trace of the event, and, as we said before, be sure to be up to date on the history if there is any issue.

Then, we query the endpoint of the customer that is associated with this webhook subscription. If it is successful we update our event history to 'delivered' if not successful it is updated to 'not_delivered'.

What is important after that is to create an endpoint for the customer to query the event history. But we won't tackle that here.

require 'uri'
require 'net/http'

class WashingJob < ApplicationJob
  queue_as :default

  def perform(customer_id)


    sub = WebHookSub.find_by(topic: 'washing',customer_id: customer_id)
      ev = EventHistory.create!({topic: 'washing',customer_id: sub.customer_id, delivery_status: 'delivering'})
      created_at = ev.created_at
      uri = URI(sub.uri)
      res = Net::HTTP.get_response(uri)

      if res.is_a?(Net::HTTPSuccess)
        EventHistory.where(customer_id: sub.customer_id, topic: 'washing', delivery_status: 'delivering', created_at:).first.update(delivery_status: 'delivered')
      else
        EventHistory.where(customer_id: sub.customer_id, topic: 'washing', delivery_status: 'delivering', created_at:).first.update(delivery_status: 'not_delivered')
      end

  end
end

Enter fullscreen mode Exit fullscreen mode

Receiver

So, the customer who wants to know if the washing job is over must give an endpoint. We need another server, though, that has an endpoint.

class WebhookEndpointController < ApplicationController
  def index 
    render :plain => "Event Accepted", :status => 202

  end
end
Enter fullscreen mode Exit fullscreen mode

In this endpoint, we responded to the API provider that we received the event with no problem. We need to do that because the API provider keeps the Event History, as we discussed earlier. If we do not respond when we want to check the event history, we will see an inconsistent state, and this could be harmful to us.

Conclusion

In this blog post, we have seen how to basically implement Webhook in Ruby. We have overlooked a lot of aspects of it. But we know that this needs work both from the API Provider side and the API Consumer side.

Moreover, we need several endpoints to make Webhooks work. And give a great developer experience to API consumers.

Top comments (3)

Collapse
 
ankitjaininfo profile image
Ankit Jain

Nice perspective on this development process.
Sometimes, as a consumer, you need to discover all required payloads. And with webhook, the first hurdle is to deploy a dummy service to print the payloads. There are online hosted tools like Beeceptor's free webhook endpoint to receive all these payloads. You can later forward this to your laptop using its Local Tunnel feature. I suggest you give this a try and improve the development process further.

(Disclaimer: Beeceptor founder here)

Collapse
 
yet_anotherdev profile image
Lucas Barret

Thanks, for your comment this would be very cool to discuss about that actually.
Would you agree that we meet sometimes ?

Collapse
 
hatemii profile image
Hatemii

replace WHERE with FIND_BY to avoid # first.update