Class: Webhukhs::ReceivedWebhook

Inherits:
ActiveRecord::Base
  • Object
show all
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

Instance Method Details

#handlerWebhukhs::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

#requestActionDispatch::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.

Returns:

  • (ActionDispatch::Request)


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.

Parameters:

  • action_dispatch_request (ActionDispatch::Request)

    request to persist



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