Class: Webhukhs::ReceivedWebhook
- Inherits:
-
ActiveRecord::Base
- Object
- ActiveRecord::Base
- Webhukhs::ReceivedWebhook
- Includes:
- StateMachineEnum
- Defined in:
- lib/webhukhs/models/received_webhook.rb
Overview
ActiveRecord model that stores received webhook requests for async processing.
Instance Method Summary collapse
-
#handler ⇒ Webhukhs::BaseHandler
Instantiates the configured handler for this webhook.
-
#request ⇒ ActionDispatch::Request
A Webhukhs handler is, in a way, a tiny Rails controller which runs in a background job.
-
#request=(action_dispatch_request) ⇒ void
Store the pertinent data from an ActionDispatch::Request into the webhook.
Instance Method Details
#handler ⇒ Webhukhs::BaseHandler
Instantiates the configured handler for this webhook.
103 104 105 |
# File 'lib/webhukhs/models/received_webhook.rb', line 103 def handler handler_module_name.constantize.new end |
#request ⇒ ActionDispatch::Request
A Webhukhs handler is, in a way, a tiny Rails controller which runs in a background job. To allow this, we need to provide access not only to the webhook payload (the HTTP request body, usually), but also to the rest of the HTTP request - such as headers and route params. For example, imagine you use a system where your multiple tenants (users) may receive webhooks from the same sender. However, you need to dispatch those webhooks to those particular tenants of your application. Instead of mounting Webhukhs as an engine under a common route, you mount it like so:
post "/incoming-webhooks/:user_id/:service_id" => "webhukhs/receive_webhooks#create"
This way, the tenant ID (the ‘user_id`) parameter is not going to be provided to you inside the webhook payload, as the sender is not sending it to you at all. However, you do have that parameter in your route. When processing the webhook, it is important for you to know which tenant has received the webhook - so that you can manipulate their data, and not the data belonging to another tenant. With validation, it is important too - in such a multitenant setup every user is likely to have their own, specific signing secret that they have set up. To find that secret and compare the signature, you need access to that `user_id` parameter.
To allow access to these, Webhukhs allows the ActionDispatch::Request object to be persisted. The persistence is not 1:1 - the Request is a fairly complex object, with lots of things injected into it by the Rails stack. Not all of those injected properties (Rack headers) are marshalable, some of them depend on the Rails application configuration, etc. However, we do retain the most important things for webhooks to be correctly handled.
-
The HTTP request body
-
The headers set by the webserver and the downstream proxies
-
The request body and query string params, depending on the MIME type
-
The route params. These are set by Journey (the Rails router) and cannot be reconstructed from a “bare” request
While this reconstruction is best-effort it might not be lossless. For example, there might be no access to Rack hijack, streaming APIs, the cookie jar or other more high-level Rails request access features. You will, however, have the basics in place - such as the params, the request body, the path params (as were decoded by your routes) etc. But it should be sufficient to do the basic tasks to process a webhook.
96 97 98 |
# File 'lib/webhukhs/models/received_webhook.rb', line 96 def request ActionDispatch::Request.new(request_headers.merge("rack.input" => StringIO.new(body.b))) end |
#request=(action_dispatch_request) ⇒ void
This method returns an undefined value.
Store the pertinent data from an ActionDispatch::Request into the webhook.
32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
# File 'lib/webhukhs/models/received_webhook.rb', line 32 def request=(action_dispatch_request) # Filter out all Rack-specific headers such as "rack.input" and the like. We are # only interested in the actual HTTP headers presented by the webserver. Mostly... headers = action_dispatch_request.env.filter_map do |header_name, header_value| if header_name.instance_of?(String) && header_name.upcase == header_name && header_value.instance_of?(String) [header_name, header_value] end end.to_h # ...except the path parameters - they do not get parsed from the headers, but instead get set by Journey - the Rails # router - when the ActionDispatch::Request object gets instantiated. They need to be preserved separately in case the Webhukhs # controller gets mounted under a parametrized path - and the path component actually is a parameter that the webhook # handler either needs for validation or for processing headers["action_dispatch.request.path_parameters"] = action_dispatch_request.env.fetch("action_dispatch.request.path_parameters") # ...and the raw request body - because we already save it separately headers.delete("RAW_POST_DATA") # Verify the request body is not too large request_body_io = action_dispatch_request.env.fetch("rack.input") if request_body_io.size > Webhukhs.configuration.request_body_size_limit raise "Cannot accept the webhook as the request body is larger than #{Webhukhs.configuration.request_body_size_limit} bytes" end write_attribute("body", request_body_io.read.b) write_attribute("request_headers", headers) ensure request_body_io.rewind end |