Class: Munster::ReceivedWebhook
- Inherits:
-
ActiveRecord::Base
- Object
- ActiveRecord::Base
- Munster::ReceivedWebhook
- Includes:
- StateMachineEnum
- Defined in:
- lib/munster/models/received_webhook.rb
Instance Method Summary collapse
- #handler ⇒ Object
-
#request ⇒ ActionDispatch::Request
A Munster handler is, in a way, a tiny Rails controller which runs in a background job.
-
#request=(action_dispatch_request) ⇒ Object
Store the pertinent data from an ActionDispatch::Request into the webhook.
Instance Method Details
#handler ⇒ Object
95 96 97 |
# File 'lib/munster/models/received_webhook.rb', line 95 def handler handler_module_name.constantize.new end |
#request ⇒ ActionDispatch::Request
A Munster 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 Munster as an engine under a common route, you mount it like so:
post "/incoming-webhooks/:user_id/:service_id" => "munster/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, Munster 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.
91 92 93 |
# File 'lib/munster/models/received_webhook.rb', line 91 def request ActionDispatch::Request.new(request_headers.merge!("rack.input" => StringIO.new(body.to_s.b))) end |
#request=(action_dispatch_request) ⇒ Object
Store the pertinent data from an ActionDispatch::Request into the webhook.
27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
# File 'lib/munster/models/received_webhook.rb', line 27 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.is_a?(String) && header_name.upcase == header_name && header_value.is_a?(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 Munster # 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 > Munster.configuration.request_body_size_limit raise "Cannot accept the webhook as the request body is larger than #{limit} bytes" end write_attribute("body", request_body_io.read.force_encoding(Encoding::BINARY)) write_attribute("request_headers", headers) ensure request_body_io.rewind end |