I’ve been recently working on a Slack bot and Slack delivers all events (webhooks) to the same URL that you can configure. That means you need to distinguish between different events types based on the payload (request body). In the past, I’ve seen that Rails apps usually reimplement their own “slack routing” within a single controller action. I wanted to share a different solution that I came up with. Let’s do routing in rails routing.
The idea is to use a custom rails constraints for that purpose. First we need a middleware that will parse the request body and store the info we need in the request env. Here is the middleware:
class EventsMiddleware
def initialize(app)
@app = app
end
def call(env)
request = Rack::Request.new(env)
if request.path == '/slack/events'
body = request.body.read
request.body.rewind
event_type = begin
JSON.parse(body)['event']['type']
rescue StandardError
nil
end
env['slack.event.type'] = event_type
end
@app.call(env)
end
end
Now we can use this middleware in our config/application.rb
:
config.middleware.use EventsMiddleware
Let’s define our custom constraint class to match against the event type:
class EventsConstraint < BaseConstraint
def self.[](*)
new(*)
end
def initialize(expected_event_type)
super()
@expected_event_type = expected_event_type
end
def matches?(request)
actual = request.env['slack.event.type'] # Set by EventsMiddleware
actual == @expected_event_type
end
end
Finally, we can use this constraint in our routes:
Rails.application.routes.draw do
post '/slack/events',
to: 'slack/messages#received',
constraints: EventsConstraint['message']
post '/slack/events',
to: 'slack/messages#mentioned',
constraints: EventsConstraint['app_mention']
post '/slack/events',
to: 'slack/home#opened',
constraints: EventsConstraint['app_home_opened']
post '/slack/events',
to: 'slack#events' # catch-all for remaining events
end
Single controller can handle either a single or multiple related event types, depending on your application domain.
I am not gonna show the whole code, but we went very similar path (middleware + constraints) with slack interactions, which are delivered to a separate URL:
post 'slack/interactions',
to: 'slack/cancellation#initiated',
constraints: BlockAction['cancel']
post 'slack/interactions',
to: 'slack/cancellation#submitted',
constraints: ViewSubmission['submit_cancellation']
I hope you find this approach useful. I find it more Rails way than reimplementing the routing logic in the controller. The only downside that I see is that the JSON body is parsed twice. However, this takes around 0.05ms, so that’s not a real performance problem compared to i.e. a single DB query.
We are testing this using request specs, which goes through the whole stack and ensures that the routing works as expected.