slackapi / bolt-js

A framework to build Slack apps using JavaScript
https://tools.slack.dev/bolt-js/
MIT License
2.75k stars 395 forks source link

deferInitialization option during App creation is not clearly explained #2071

Open mfb-remotesocial opened 9 months ago

mfb-remotesocial commented 9 months ago

It has taken me significantly longer than I would like to admit to debug and triage an issue in our Google Firebase app that utilises Slack Bolt.

It appears our app was incorrectly throwing an error outside of a promise, resulting in early termination of the cloud function process.

I noticed an unhandled error was causing our cloud function to terminate early when attempting to call the Slack API (via Slack Bolt) with an incorrect or invalid access_token. This would circumvent my try/catch statements and immediately terminate the cloud function. Even when removing the call to the Slack API, the error was persisting, which was super weird as I was no longer even invoking the Slack API, yet was still seeing a Slack API error.

After much trial and error, it turns out the issue is related to how the Slack Bolt app is instantiated before sending the request to Slack. It appears this is a known error as I have been able to discover an initialization param (deferInitialization) which returns the App class without calling the internal init() and start() methods (which ultimately throw an error by making an async call to the auth.test endpoint).

It would be good to better document the use case for this config setting and call out specifically that this is useful for users of FaaS solutions (like AWS or Firebase), as this can be very hard to diagnose as an issue.

My specific workaround was to create an async function that returns the App class after awaiting the init() and start() steps. Doing this allows the app to pause running and properly throw any errors generated during the init() and start() process. In this case, it correctly throws an error about the app credentials being invalid, however it is now doing so within the normal async/await workflow.

An example of the issue (roughly mocked up for brevity):

const revokeAuth = async (accessToken) => {
    try {
        console.log('initialising app');
        const app = new App({ receiver, token: accessToken });

        console.log('calling Slack API);
        const response = await app.client.auth.revoke();

        return response.ok;
    } catch (err) {
        console.error('Inside Catch:: Error calling auth.revoke', err.message);
    }
}

Expected output:

> initialising app
> calling Slack API
> Inside Catch:: Error calling auth.revoke Error: An API error occurred: account_inactive...

Actual output in Firebase:

> initialise app
> calling Slack API
> .../@slack/web-api/dist/errors.js:62
>      const error = errorWithCode(new Error(`An API error occurred: ${result.error}`), ErrorCode.PlatformError);

> Error: An API error occurred: account_inactive...
> ...

> Node.js v20.11.1

Note that the catch statement has never fired; more importantly, the firebase function terminated early. If I had required additional steps to proceed even if this step fails, those would have all been terminated without running.

Removing the direct call to the slack API did not fix the issue, and trying to add an await statement to the new App statement has no effect. My solution was to:

const createApp = async (accessToken) => {
    const app = new App({ receiver, token: accessToken, deferInitialization: true });
    await app.init();
    await app.start();
    return app;
};

const revokeAuth = async (accessToken) => {
    try {
        const app = await createApp(accessToken);
        const response = await app.client.auth.revoke();
    } catch (err) {
        console.error('Inside Catch:: Error calling auth.revoke', err.message);
        ... < do other stuff related to error handling here >
    }
}

This second approach now always triggers the catch statement by correctly awaiting the initialization of the App before moving along.

This also highlights that the Slack Bolt App makes a call to the Slack API with the provided access_token during app.init(), before any requested API endpoint is called, which is also not adequately called out in the docs.

seratch commented 9 months ago

Hi @mfb-remotesocial, thank you so much for the detailed feedback and we are sorry for the confusion you've experienced with the initialization process behavior.

Although the option is mentioned here in the document, the section does not mention any specific benefits with the option. This can be quickly improved so we will take actions soon. Thanks again for your time to share this!

mfb-remotesocial commented 9 months ago

Hi @seratch, Thanks so much for following this up. I had seen the section around deferInitialization when first reading the docs. However, it was not clear to me at the time why I would ever need that feature, as there was no indication that an async call is made to Slack during the init() phase. If you are able to add this to the docs, it may help others in the future. Thanks again. I really appreciate the follow-up.