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
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
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
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
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)
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)
Thanks, for your comment this would be very cool to discuss about that actually.
Would you agree that we meet sometimes ?
replace WHERE with FIND_BY to avoid # first.update