diff --git a/README.md b/README.md index 1875085a3..e1688124d 100644 --- a/README.md +++ b/README.md @@ -256,7 +256,6 @@ async function authWithAcme({ payload, context, say, next }) { // When the user lookup is successful, add the user details to the context context.user = user; } catch (error) { - // middleware/listeners continue if (error.message === 'Not Found') { // In the real world, you would need to check if the say function was defined, falling back to the respond // function if not, and then falling back to only logging the error as a last resort. @@ -327,7 +326,7 @@ In general, a middleware can run both before and after the remaining middleware How you use `next` can have four different effects: -* **To both preprocess and post-process events** - You can choose to do work going _before_ listener functions by putting code +* **To both preprocess and post-process events** - You can choose to do work both _before_ listener functions by putting code before `await next()` and _after_ by putting code after `await next()`. `await next()` passes control down the middleware stack in the order it was defined, then back up it in reverse order. diff --git a/docs/_advanced/middleware_global.md b/docs/_advanced/middleware_global.md index 02b6af852..b4b7ecf30 100644 --- a/docs/_advanced/middleware_global.md +++ b/docs/_advanced/middleware_global.md @@ -6,7 +6,7 @@ order: 4 ---
-Global middleware is run for all incoming events before any listener middleware. You can add any number of global middleware to your app by utilizing `app.use(fn({payload,...,next}))`. +Global middleware is run for all incoming events before any listener middleware. You can add any number of global middleware to your app by utilizing `app.use(fn)`. The middleware function `fn` is called with the same arguments as listeners and an additional `next` function. Both global and listener middleware must call `await next()` to pass control of the execution chain to the next middleware, or call `throw` to pass an error back up the previously-executed middleware chain. diff --git a/docs/_advanced/receiver.md b/docs/_advanced/receiver.md index 87e189f0c..eb9af501a 100644 --- a/docs/_advanced/receiver.md +++ b/docs/_advanced/receiver.md @@ -15,11 +15,11 @@ A receiver is responsible for handling and parsing any incoming events from Slac | `stop()` | None | `Promise` | `init()` is called after Bolt for JavaScript app is created. This method gives the receiver a reference to an `App` to store so that it can call: -* `await app.processEvent(event)` whenever your app receives an event from Slack. It will reject if there is an unhandled error. +* `await app.processEvent(event)` whenever your app receives an event from Slack. It will throw if there is an unhandled error. To use a custom receiver, you can pass it into the constructor when initializing your Bolt for JavaScript app. Here is what a basic custom receiver might look like. -For a more in-depth look at a receiver, [read the source code for the built-in Express receiver](https://github.com/slackapi/bolt/blob/master/src/ExpressReceiver.ts) +For a more in-depth look at a receiver, [read the source code for the built-in `ExpressReceiver`](https://github.com/slackapi/bolt/blob/master/src/ExpressReceiver.ts)
```javascript diff --git a/docs/_basic/listening_responding_shortcuts.md b/docs/_basic/listening_responding_shortcuts.md index c0db2d905..5d3227eab 100644 --- a/docs/_basic/listening_responding_shortcuts.md +++ b/docs/_basic/listening_responding_shortcuts.md @@ -82,7 +82,7 @@ app.shortcut('open_modal', async ({ shortcut, ack, context, client }) => { ```javascript // Your middleware will only be called when the callback_id matches 'open_modal' AND the type matches 'message_action' - app.shortcut({ callback_id: 'open_modal', type: 'message_action' }, async ({ action, ack, context, client }) => { + app.shortcut({ callback_id: 'open_modal', type: 'message_action' }, async ({ shortcut, ack, context, client }) => { try { // Acknowledge shortcut request await ack(); diff --git a/src/App.spec.ts b/src/App.spec.ts index ad04612ac..e96ef3419 100644 --- a/src/App.spec.ts +++ b/src/App.spec.ts @@ -440,7 +440,6 @@ describe('App', () => { app.error(async (actualError) => { assert.instanceOf(actualError, UnknownError); assert.equal(actualError.message, error.message); - await delay(); // Make this async to make sure error handlers can be tested }); await fakeReceiver.sendEvent(dummyReceiverEvent); diff --git a/src/ExpressReceiver.ts b/src/ExpressReceiver.ts index 7230a338b..19a37ea7c 100644 --- a/src/ExpressReceiver.ts +++ b/src/ExpressReceiver.ts @@ -1,12 +1,12 @@ import { AnyMiddlewareArgs, Receiver, ReceiverEvent } from './types'; import { createServer, Server } from 'http'; -import express, { Request, Response, Application, RequestHandler, NextFunction } from 'express'; +import express, { Request, Response, Application, RequestHandler } from 'express'; import rawBody from 'raw-body'; import querystring from 'querystring'; import crypto from 'crypto'; import tsscmp from 'tsscmp'; import App from './App'; -import { ReceiverAuthenticityError, ReceiverAckTimeoutError, ReceiverMultipleAckError } from './errors'; +import { ReceiverAuthenticityError, ReceiverMultipleAckError } from './errors'; import { Logger, ConsoleLogger } from '@slack/logger'; // TODO: we throw away the key names for endpoints, so maybe we should use this interface. is it better for migrations? @@ -29,6 +29,7 @@ export default class ExpressReceiver implements Receiver { private server: Server; private bolt: App | undefined; + private logger: Logger; constructor({ signingSecret = '', @@ -36,7 +37,6 @@ export default class ExpressReceiver implements Receiver { endpoints = { events: '/slack/events' }, }: ExpressReceiverOptions) { this.app = express(); - this.app.use(this.errorHandler.bind(this)); // TODO: what about starting an https server instead of http? what about other options to create the server? this.server = createServer(this.app); @@ -47,6 +47,7 @@ export default class ExpressReceiver implements Receiver { this.requestHandler.bind(this), ]; + this.logger = logger; const endpointList: string[] = typeof endpoints === 'string' ? [endpoints] : Object.values(endpoints); for (const endpoint of endpointList) { this.app.post(endpoint, ...expressMiddleware); @@ -54,33 +55,28 @@ export default class ExpressReceiver implements Receiver { } private async requestHandler(req: Request, res: Response): Promise { - let timer: NodeJS.Timeout | undefined = setTimeout( - () => { - this.bolt?.handleError(new ReceiverAckTimeoutError( - 'An incoming event was not acknowledged before the timeout. ' + - 'Ensure that the ack() argument is called in your listeners.', - )); - timer = undefined; - }, - 2800, - ); + let isAcknowledged = false; + setTimeout(() => { + if (!isAcknowledged) { + this.logger.error('An incoming event was not acknowledged within 3 seconds. ' + + 'Ensure that the ack() argument is called in a listener.'); + } + // tslint:disable-next-line: align + }, 3001); const event: ReceiverEvent = { body: req.body, - ack: async (response): Promise => { - if (timer !== undefined) { - clearTimeout(timer); - timer = undefined; - - if (!response) { - res.send(''); - } else if (typeof response === 'string') { - res.send(response); - } else { - res.json(response); - } + ack: async (response: any): Promise => { + if (isAcknowledged) { + throw new ReceiverMultipleAckError(); + } + isAcknowledged = true; + if (!response) { + res.send(''); + } else if (typeof response === 'string') { + res.send(response); } else { - this.bolt?.handleError(new ReceiverMultipleAckError()); + res.json(response); } }, }; @@ -129,12 +125,6 @@ export default class ExpressReceiver implements Receiver { }); }); } - - private errorHandler(err: any, _req: Request, _res: Response, next: NextFunction): void { - this.bolt?.handleError(err); - // Forward to express' default error handler (which knows how to print stack traces in development) - next(err); - } } export const respondToSslCheck: RequestHandler = (req, res, next) => { diff --git a/src/errors.spec.ts b/src/errors.spec.ts index b06e6e5ec..ac56a2819 100644 --- a/src/errors.spec.ts +++ b/src/errors.spec.ts @@ -7,8 +7,8 @@ import { AppInitializationError, AuthorizationError, ContextMissingPropertyError, - ReceiverAckTimeoutError, ReceiverAuthenticityError, + ReceiverMultipleAckError, UnknownError, } from './errors'; @@ -19,8 +19,8 @@ describe('Errors', () => { [ErrorCode.AppInitializationError]: new AppInitializationError(), [ErrorCode.AuthorizationError]: new AuthorizationError('auth failed', new Error('auth failed')), [ErrorCode.ContextMissingPropertyError]: new ContextMissingPropertyError('foo', "can't find foo"), - [ErrorCode.ReceiverAckTimeoutError]: new ReceiverAckTimeoutError(), [ErrorCode.ReceiverAuthenticityError]: new ReceiverAuthenticityError(), + [ErrorCode.ReceiverMultipleAckError]: new ReceiverMultipleAckError(), [ErrorCode.UnknownError]: new UnknownError(new Error('It errored')), }; diff --git a/src/errors.ts b/src/errors.ts index f8150e9c6..0e20a75b2 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -8,8 +8,7 @@ export enum ErrorCode { ContextMissingPropertyError = 'slack_bolt_context_missing_property_error', - ReceiverAckTimeoutError = 'slack_bolt_receiver_ack_timeout_error', - ReceiverAckTwiceError = 'slack_bolt_receiver_ack_twice_error', + ReceiverMultipleAckError = 'slack_bolt_receiver_ack_multiple_error', ReceiverAuthenticityError = 'slack_bolt_receiver_authenticity_error', /** @@ -52,12 +51,8 @@ export class ContextMissingPropertyError extends Error implements CodedError { } } -export class ReceiverAckTimeoutError extends Error implements CodedError { - public code = ErrorCode.ReceiverAckTimeoutError; -} - export class ReceiverMultipleAckError extends Error implements CodedError { - public code = ErrorCode.ReceiverAckTimeoutError; + public code = ErrorCode.ReceiverMultipleAckError; constructor() { super("The receiver's `ack` function was called multiple times.");