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/ja_receiver.md b/docs/_advanced/ja_receiver.md
index 18dfabaf1..bb54a3629 100644
--- a/docs/_advanced/ja_receiver.md
+++ b/docs/_advanced/ja_receiver.md
@@ -14,13 +14,12 @@ order: 8
| `start()` | None | `Promise` |
| `stop()` | None | `Promise` |
-Bolt アプリでは `init()` が 2 回呼び出されます。
-* `await app.processEvent(event)` は、解析されたすべての着信リクエストを `onIncomingEvent()` にルーティングする必要があります。これは、Bolt アプリでは `this.receiver.on('message', message => this.onIncomingEvent(message))` として呼び出されます。
-* `await app.handleError` は、エラーをグローバルエラーハンドラーにルーティングする必要があります。これは、Bolt アプリでは `this.receiver.on('error', error => this.onGlobalError(error))` として呼び出されます。
+Bolt アプリでは `init()` が 2 回呼び出されます。このメソッドはレシーバーに `App` の参照を付与するため、これにより以下の呼び出しが可能になります。
+* `await app.processEvent(event)` は Slack から送信されてくるイベントを受け取るたびに呼び出されます。ハンドリングされなかったエラーが発生した場合はそれを throw します。
Bolt アプリを初期化するときにカスタムレシーバーをコンストラクタに渡すことで、そのカスタムレシーバーを使用できます。ここで紹介するのは、基本的なカスタムレシーバーです。
-レシーバーについて詳しくは、[組み込み Express レシーバーのソースコード](https://github.com/slackapi/bolt/blob/master/src/ExpressReceiver.ts)をお読みください。
+レシーバーについて詳しくは、[組み込み `ExpressReceiver` のソースコード](https://github.com/slackapi/bolt/blob/master/src/ExpressReceiver.ts)をお読みください。
```javascript
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/ja_ listening_responding_shortcuts.md b/docs/_basic/ja_ listening_responding_shortcuts.md
deleted file mode 100644
index 2e9581085..000000000
--- a/docs/_basic/ja_ listening_responding_shortcuts.md
+++ /dev/null
@@ -1,65 +0,0 @@
----
-title: グローバルショートカットのリスニング
-lang: ja-jp
-slug: shortcuts
-order: 8
----
-
-
-[グローバルショートカット](https://api.slack.com/interactivity/shortcuts/using#global_shortcuts)は、テキスト入力エリアや検索バーから起動できる Slack クライアント内の UI エレメントです。`shortcut()` メソッドを使って、グローバルショートカットのイベントをリスニングすることができます。このメソッドには `callback_id` を文字列または正規表現のデータ型で設定します。
-
-グローバルショートカットのイベントは Slack へイベントを受信したことを知らせるために `ack()` メソッドで確認する必要があります。
-
-グローバルショートカットのペイロードは、ユーザーの実行アクションの確認のために[モーダルを開く](#creating-modals)などの用途に使用できる `trigger_id` を含んでいます。グローバルショートカットのペイロードは **チャンネル ID は含んでいない** ことに注意してください。もしあなたのアプリがチャンネル ID を知る必要があれば、モーダル内で [`conversations_select`](https://api.slack.com/reference/block-kit/block-elements#conversation_select) エレメントを使用できます。
-
-⚠️ [メッセージショートカット](https://api.slack.com/interactivity/shortcuts/using#message_shortcuts)は引き続き[`action()` メソッド](#action-listening)を使用するので注意してください。**Bolt の次のメジャーバージョンからはグローバルショートカットもメッセージショートカットも両方とも `shortcut()` メソッドを使用します。**
-
-
-```javascript
-// open_modal というグローバルショートカットはシンプルなモーダルを開く
-app.shortcut('open_modal', async ({ payload, ack, context }) => {
- // グローバルショートカットリクエストの確認
- ack();
- try {
- // 組み込みの WebClient を使って views.open API メソッドを呼び出す
- const result = await app.client.views.open({
- // `context` オブジェクトに保持されたトークンを使用
- token: context.botToken,
- trigger_id: payload.trigger_id,
- view: {
- "type": "modal",
- "title": {
- "type": "plain_text",
- "text": "My App"
- },
- "close": {
- "type": "plain_text",
- "text": "Close"
- },
- "blocks": [
- {
- "type": "section",
- "text": {
- "type": "mrkdwn",
- "text": "About the simplest modal you could conceive of :smile:\n\nMaybe or ."
- }
- },
- {
- "type": "context",
- "elements": [
- {
- "type": "mrkdwn",
- "text": "Psssst this modal was designed using "
- }
- ]
- }
- ]
- }
- });
- console.log(result);
- }
- catch (error) {
- console.error(error);
- }
-});
-```
diff --git a/docs/_basic/ja_listening_responding_shortcuts.md b/docs/_basic/ja_listening_responding_shortcuts.md
new file mode 100644
index 000000000..0d02fbf6b
--- /dev/null
+++ b/docs/_basic/ja_listening_responding_shortcuts.md
@@ -0,0 +1,133 @@
+---
+title: ショートカットのリスニング
+lang: ja-jp
+slug: shortcuts
+order: 8
+---
+
+
+`shortcut()` メソッドは、[グローバルショートカット](https://api.slack.com/interactivity/shortcuts/using#global_shortcuts)と[メッセージショートカット](https://api.slack.com/interactivity/shortcuts/using#message_shortcuts)の両方をサポートします。
+
+ショートカットは、テキスト入力エリアや検索バーから起動できる Slack クライアント内の UI エレメントです。グローバルショートカットは、コンポーザーメニューまたは検索メニューから呼び出すことができます。メッセージショートカットは、メッセージのコンテキストメニュー内にあります。`shortcut()` メソッドを使って、これらのショートカットのイベントをリスニングすることができます。このメソッドには `callback_id` を文字列または正規表現のデータ型で設定します。
+
+グローバルショートカットのイベントは Slack へイベントを受信したことを知らせるために `ack()` メソッドで確認する必要があります。
+
+グローバルショートカットのペイロードは、ユーザーの実行アクションの確認のために[モーダルを開く](#creating-modals)などの用途に使用できる `trigger_id` を含んでいます。
+
+⚠️ グローバルショートカットのペイロードは **チャンネル ID は含んでいない** ことに注意してください。もしあなたのアプリがチャンネル ID を知る必要があれば、モーダル内で [`conversations_select`](https://api.slack.com/reference/block-kit/block-elements#conversation_select) エレメントを使用できます。
+メッセージショートカットのペイロードはチャンネル ID を含みます。
+
+
+```javascript
+// open_modal というグローバルショートカットはシンプルなモーダルを開く
+app.shortcut('open_modal', async ({ shortcut, ack, context }) => {
+ // グローバルショートカットリクエストの確認
+ ack();
+
+ try {
+ // 組み込みの WebClient を使って views.open API メソッドを呼び出す
+ const result = await app.client.views.open({
+ // `context` オブジェクトに保持されたトークンを使用
+ token: context.botToken,
+ trigger_id: shortcut.trigger_id,
+ view: {
+ "type": "modal",
+ "title": {
+ "type": "plain_text",
+ "text": "My App"
+ },
+ "close": {
+ "type": "plain_text",
+ "text": "Close"
+ },
+ "blocks": [
+ {
+ "type": "section",
+ "text": {
+ "type": "mrkdwn",
+ "text": "About the simplest modal you could conceive of :smile:\n\nMaybe or ."
+ }
+ },
+ {
+ "type": "context",
+ "elements": [
+ {
+ "type": "mrkdwn",
+ "text": "Psssst this modal was designed using "
+ }
+ ]
+ }
+ ]
+ }
+ });
+
+ console.log(result);
+ }
+ catch (error) {
+ console.error(error);
+ }
+});
+```
+
+
+
+ 制約付きオブジェクトを使用したショートカットのリスニング
+
+
+
+ 制約付きオブジェクトを使って `callback_id` や `type` によるリスニングをすることができます。オブジェクト内の制約は文字列型または RegExp オブジェクトを使用できます。
+
+
+
+ ```javascript
+ // callback_id が 'open_modal' と一致し type が 'message_action' と一致する場合のみミドルウェアが呼び出される
+ app.shortcut({ callback_id: 'open_modal', type: 'message_action' }, async ({ shortcut, ack, context, client }) => {
+ try {
+ // ショートカットリクエストの確認
+ await ack();
+
+ // 組み込みの WebClient を使って views.open API メソッドを呼び出す
+ const result = await app.client.views.open({
+ // `context` オブジェクトに保持されたトークンを使用
+ token: context.botToken,
+ trigger_id: shortcut.trigger_id,
+ view: {
+ type: "modal",
+ title: {
+ type: "plain_text",
+ text: "My App"
+ },
+ close: {
+ type: "plain_text",
+ text: "Close"
+ },
+ blocks: [
+ {
+ type: "section",
+ text: {
+ type: "mrkdwn",
+ text: "About the simplest modal you could conceive of :smile:\n\nMaybe or ."
+ }
+ },
+ {
+ type: "context",
+ elements: [
+ {
+ type: "mrkdwn",
+ text: "Psssst this modal was designed using "
+ }
+ ]
+ }
+ ]
+ }
+ });
+
+ console.log(result);
+ }
+ catch (error) {
+ console.error(error);
+ }
+ });
+ ```
+
+
\ No newline at end of file
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/docs/_tutorials/ja_getting_started.md b/docs/_tutorials/ja_getting_started.md
index 2a137e80c..738ef479d 100644
--- a/docs/_tutorials/ja_getting_started.md
+++ b/docs/_tutorials/ja_getting_started.md
@@ -191,7 +191,7 @@ app.message('hello', async ({ message, say }) => {
### アクションの送信と応答
-ボタン、選択メニュー、日付ピッカー、ダイアログ、メッセージショートカットなどの機能を使用するには、インタラクティブ性を有効にする必要があります。イベントと同様に、Slack の URL を指定してアクション ( 「ボタン・クリック」など) を送信する必要があります。
+ボタン、選択メニュー、日付ピッカー、ダイアログなどの機能を使用するには、インタラクティブ性を有効にする必要があります。イベントと同様に、Slack の URL を指定してアクション ( 「ボタン・クリック」など) を送信する必要があります。
アプリ設定ページに戻り、左側の **Interactive Components** をクリックします。**Request URL** ボックスがもう 1 つあることがわかります。
diff --git a/src/App.spec.ts b/src/App.spec.ts
index ad04612ac..8f86b3e8d 100644
--- a/src/App.spec.ts
+++ b/src/App.spec.ts
@@ -4,7 +4,7 @@ import sinon, { SinonSpy } from 'sinon';
import { assert } from 'chai';
import { Override, mergeOverrides, createFakeLogger, delay } from './test-helpers';
import rewiremock from 'rewiremock';
-import { ErrorCode, UnknownError } from './errors';
+import { ErrorCode, UnknownError, AuthorizationError } from './errors';
import { Receiver, ReceiverEvent, SayFn, NextMiddleware } from './types';
import { ConversationStore } from './conversation-store';
import { LogLevel } from '@slack/logger';
@@ -288,11 +288,12 @@ describe('App', () => {
assert.isAtLeast(fakeLogger.warn.callCount, invalidReceiverEvents.length);
});
- it('should warn and skip when a receiver event fails authorization', async () => {
+ it('should warn, send to global error handler, and skip when a receiver event fails authorization', async () => {
// Arrange
const fakeLogger = createFakeLogger();
const fakeMiddleware = sinon.fake(noopMiddleware);
- const dummyAuthorizationError = new Error();
+ const dummyOrigError = new Error('auth failed');
+ const dummyAuthorizationError = new AuthorizationError('auth failed', dummyOrigError);
const dummyReceiverEvent = createDummyReceiverEvent();
const App = await importApp(); // tslint:disable-line:variable-name
@@ -309,6 +310,9 @@ describe('App', () => {
// Assert
assert(fakeMiddleware.notCalled);
assert(fakeLogger.warn.called);
+ assert.instanceOf(fakeErrorHandler.firstCall.args[0], Error);
+ assert.propertyVal(fakeErrorHandler.firstCall.args[0], 'code', ErrorCode.AuthorizationError);
+ assert.propertyVal(fakeErrorHandler.firstCall.args[0], 'original', dummyAuthorizationError.original);
});
describe('global middleware', () => {
@@ -440,7 +444,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.");