Bolt-js: ExpressReceiver app property should be public

Created on 15 May 2019  路  11Comments  路  Source: slackapi/bolt-js

Description

The ExpressReceiver class defines a private property called app, which is an Express application. Developers who want to add additional routes to their server would need access to this value. We can enable those use cases by making the property public.

Drawbacks considered:

  • Making it public means we have to maintain it as a stable part of the API: this should be fine because no matter what our implementation, the point of the ExpressReceiver is to expose the fact that it's using express. Otherwise, why not just use http.createServer directly?
  • Developers might destructively modify the behavior of the application, and that can make Bolt unstable: this is a risk that can be mitigated with documentation around how you should and should not the ExpressReceiver. The advice for a developer who needs a higher level of control than what we intend with ExpressReceiver is to create their own Receiver implementation.

Requirements (place an x in each of the [ ])

  • [x] I've read and understood the Contributing guidelines and have done my best effort to follow them.
  • [x] I've read and agree to the Code of Conduct.
  • [x] I've searched for any related issues and avoided creating a duplicate issue.
enhancement

Most helpful comment

I ran into this issue when I was writing an example which runs on AWS Lambda (+ Serverless Framework). I needed to expose raw Express Application object as a AWS Lambda handler. But accessing app.receiver.app means relying on a private field as this issue describes.

I dealt with it by manually creating ExpressReceiver object outside of Bolt framework and accessing app field legally.

With this approach, it's also possible to add routes to Express's Application after the intiialization: https://github.com/seratch/slack-app-examples/blob/9ed171575363eacfc54699188bba1dafdbd1a4ae/serverless-bolt-template/aws-ts/app.ts#L133-L166

In my opinion, it may be enough to just recommend users to create ExpressReceiver object on the user side. But it's fair enough to make receiver.app public, too.

All 11 comments

An alternative to making app public is to do something similar to what _Probot_ is doing:

https://probot.github.io/docs/http/

You can ask it for an Express Router, on which you can then install your own handlers and middleware.

I ran into this issue when I was writing an example which runs on AWS Lambda (+ Serverless Framework). I needed to expose raw Express Application object as a AWS Lambda handler. But accessing app.receiver.app means relying on a private field as this issue describes.

I dealt with it by manually creating ExpressReceiver object outside of Bolt framework and accessing app field legally.

With this approach, it's also possible to add routes to Express's Application after the intiialization: https://github.com/seratch/slack-app-examples/blob/9ed171575363eacfc54699188bba1dafdbd1a4ae/serverless-bolt-template/aws-ts/app.ts#L133-L166

In my opinion, it may be enough to just recommend users to create ExpressReceiver object on the user side. But it's fair enough to make receiver.app public, too.

I ended up doing it the other way around: I wrote a new Receiver that does not create and own Express - instead it takes an Express app on which it can mount it's own /slack/events handler.

Personally I like this approach because I think in most cases you want your Slack bot to be part of a bigger app that you are writing; probably at least to handle OAuth. Possibly also to have an admin interface or some additional APIs or even webhooks for other services you integrate with.

(Bolt handling OAuth out of the box would be even better of course. It would turn Bolt into an actual complete installable Slack Application. If there is interest in that I'm happy to write a more complete story in a new issue.)

I'm starting to agree with @seratch's approach here. Just to nail this down, what he recommended boils down to this:

const { App, ExpressReceiver } = require('@slack/bolt');

// Initialize your own ExpressReceiver
const receiver = new ExpressReceiver({ signingSecret: process.env.SLACK_SIGNING_SECRET, endpoints: '/slack/events' });

// Mount additional routes (does not depend on accessing private properties)
receiver.app.get('/foo', () => { /* ... */ });

// Specify your receiver as the custom receiver for the bolt App
const app = new App({ token: 'blah', receiver });

I like this approach because it retains a smaller public API surface since the App.receiver property is not public. That means in the future the Bolt implementation of App can change and it wouldn't affect your any apps using Bolt.

I ended up doing it the other way around: I wrote a new Receiver that does not create and own Express - instead it takes an Express app on which it can mount it's own /slack/events handler.

@st3fan This is great and I think you're ahead of the curve. The Receiver interface was intentionally designed so that users could supply their own implementation. We further hope that in the future people will share their receiver implementations with the community. For example, if you want to use hapi instead of Express, you can write a reciever implementation which uses hapi and share that as a package (maybe bolt-receiver-hapi) for others with that same opinion to use.

It sounds like you want to use express but you want the receiver to accept an app on initialization. That's a valid feature request for the built-in ExpressReceiver that the community can discuss. Would you like to open a ticket for that? If that feature lands, then you would still use the custom receiver example I showed above, but the constructor for ExpressReceiver would take an additional option.

If you do open that feature request, one disadvantage to think through is that we cannot be sure what sort of processing is happening by the routes/middleware that are mounted before the ExpressReceiver constructor runs. This opens the door for some bad things, because it means that an earlier middleware could have decoded the buffered body without sharing the raw body with later handlers. That kind of a problem would prevent request signature verification to work. What advantages are there that for this approach?

Personally I like this approach because I think in most cases you want your Slack bot to be part of a bigger app that you are writing; probably at least to handle OAuth. Possibly also to have an admin interface or some additional APIs or even webhooks for other services you integrate with.

This makes sense to me, but I'm not sure why you would be limited in any way by allowing the ExpressReceiver to initialize the Express app.

(Bolt handling OAuth out of the box would be even better of course. It would turn Bolt into an actual complete installable Slack Application. If there is interest in that I'm happy to write a more complete story in a new issue.)

This could be another valid feature request for the ExpressReceiver. If you'd like to see this feature added, can you open another issue?

I'll close this issue because it seems like we've found a preferred alternative 鈽濓笍 .

Feel free to continue commenting if you feel there may still be a need to make app.receiver a public property.

@aoberoi Has this feature been worked on? I've run into this problem now when trying to distribute my Slack app to different workspaces. I need to setup an endpoint on my bot's server to handle the redirect_uri to get the temporary code and make the request to get an access token.

@evanhli did you find any way to make this work?

I tried the suggestion:

const { App, ExpressReceiver } = require('@slack/bolt');

// Initialize your own ExpressReceiver
const receiver = new ExpressReceiver({ signingSecret: process.env.SLACK_SIGNING_SECRET, endpoints: '/slack/events' });

// Mount additional routes (does not depend on accessing private properties)
receiver.app.get('/foo', () => { /* ... */ });

// Specify your receiver as the custom receiver for the bolt App
const app = new App({ token: 'blah', receiver });

But express returns Cannot GET /foo. I think something in the constructor and binding for the built-in receiver is borked. If I "hack" a new route directly in the receiver's constructor it works fine.

Blargh I see what I did. You cannot pass signingSecret into App, or the App constructor will create a new Express app instance, rendering your predefined receiver useless.

@talbright Did you get it working? I removed my signingsecret from my App so my code looks like this now:

const { App, ExpressReceiver } = require('@slack/bolt');

// Initialize your own ExpressReceiver
const receiver = new ExpressReceiver({ 
  signingSecret: <SIGNING_SECRET>, 
  endpoints: '/slack/events',
});

// Mount additional routes (does not depend on accessing private properties)
receiver.app.get('/foo', () => {
  console.log('test foo endpoint')
});

// Specify your receiver as the custom receiver for the bolt App
const app = new App({
  token: <TOKEN>,
  receiver,
});

But i'm still getting 404 on GET to /foo

Edit: Fixed token bug mentioned below

Nvm I was running a completely different App, oops. The above should work fine for future reference.

Yes I did. One bug in your code above, you have to pass the token to the App constructor, not the ExpressReceiver constructor.

Was this page helpful?
0 / 5 - 0 ratings