slackapi / node-slack-sdk

Slack Developer Kit for Node.js

Home Page:https://slack.dev/node-slack-sdk

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Main thread crashes due to unhandled error from @slack/web-api SDK

sergio-toro opened this issue · comments

This error is the same as #1626 which was closed, but we are getting it in the last released version of the SDK

Packages:

Select all that apply:

  • @slack/web-api
  • @slack/rtm-api
  • @slack/webhooks
  • @slack/oauth
  • @slack/socket-mode
  • @slack/types
  • I don't know

Reproducible in:

The Slack SDK version

"slack/oauth": "^3.0.0",
"slack/web-api": "^7.0.2",

Node.js runtime version

v18.19.0

OS info

amazonlinux:2023.2.20231113.0

Steps to reproduce:

The app is running on long thread, sometimes the WebClient just crashes when doing a request and instead of rejecting the promise that is awaiting the response it bubbles up the error and kills the main thread

Expected result:

The request to Slack rejects the call due to a legit error (account_inactive) and the error can be matched.

try {
	const client = new WebClient()
	const { ok, user } = await client.users.lookupByEmail({
		token: botToken,
		email,
	});
} catch(error) {
	console.error("Slack request error", error);
}

Actual result:

// The error on the process uncaughtException listener
"code": "slack_webapi_platform_error",
"data": {
	"error": "account_inactive",
	"ok": false
}

// Additional stack trace
Error: An API error occurred: account_inactive
    at platformErrorFromResult (/srv/node_modules/@slack/web-api/dist/errors.js:62:33)
    at WebClient.<anonymous> (/srv/node_modules/@slack/web-api/dist/WebClient.js:198:60)
    at Generator.next (<anonymous>)
    at fulfilled (/srv/node_modules/@slack/web-api/dist/WebClient.js:28:58)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)

Code that represents what is happening

process.on("uncaughtException", (error, origin) => {
	console.error("Unhandled Slack error:", {
		error,
		origin,
	});
});

doSlackRequest("some-email@test.com", "valid-token")
	.then(/* ... */)
	.catch((error) => {
		console.error("Never gets executed", error);
	});

async function doSlackRequest(email, token) {
	try {
		const client = new WebClient()
		const { ok, user } = await client.users.lookupByEmail({
			token,
			email,
		});
		console.log("Slack request ok", user);
	} catch(error) {
		console.error("Slack request error", error);
	}
}

Hi @sergio-toro, thank you so much for taking the time to report this and we're sorry for the disruption you've encountered.

With given information, I am still unsure how to reproduce the crash as it does not usually happen. How are you running your doSlackRequest function? Is it a part of a web app / Slack app? We'll try to reproduce it on our end, but sharing repro steps to see the same situation easily would be very much appreciated.

Hi @sergio-toro, thank you so much for taking the time to report this and we're sorry for the disruption you've encountered.

With given information, I am still unsure how to reproduce the crash as it does not usually happen. How are you running your doSlackRequest function? Is it a part of a web app / Slack app? We'll try to reproduce it on my end, but sharing repro steps to see the same situation easily would be very much appreciated.

The challenge here is that this error does not seem to be reproducible easily... sometimes we run for hours/days without issues (with errors being thrown and being able to catch them) and then thrown to the main thread without anything else in the logs.

More context:
Our product uses an integration with Slack to send message Notification to the users (about they booking a session with us, or reminders of the sessions).

Here is how we initialize the Repository on the server:

import { singleton } from "tsyringe";
import { UsersLookupByEmailResponse, WebClient } from "@slack/web-api";
import { logger } from '@olivahealth/logger/server';

@singleton()
export default class SlackRepository extends PrismaAdapter {
  private client: WebClient;
  constructor() {
    super();
    this.client = new WebClient();
  }
  async findByEmail(
    clientToken: string,
    email: string,
  ): Promise<UsersLookupByEmailResponse["user"]> {
    try {
      const { ok, user } = await this.client.users.lookupByEmail({
        token: clientToken,
        email,
      });
      if (!ok) throw new Error(`No slack user found with ${email}`);
  
      return user;
    } catch (error) {
      logger.error("SlackRepository", "findByEmail had an error",  error);
      throw error;
    }
  }
}

Reviewing this issue, a couple of things.

First, regarding the following code sample you provided:

doSlackRequest("some-email@test.com", "valid-token")
	.then(/* ... */)
	.catch((error) => {
		console.error("Never gets executed", error);
	});

async function doSlackRequest(email, token) {
	try {
		const client = new WebClient()
		const { ok, user } = await client.users.lookupByEmail({
			token,
			email,
		});
		console.log("Slack request ok", user);
	} catch(error) {
		console.error("Slack request error", error);
	}
}

The catch() chain in the promise would never execute as the doSlackRequest will never raise an exception - the entire function is wrapped in a try/catch and nothing in the catch {} block is re-thrown. Therefore, the catch() promise will never execute.

Second, regarding this code snippet:

export default class SlackRepository extends PrismaAdapter {
  private client: WebClient;
  constructor() {
    super();
    this.client = new WebClient();
  }
  async findByEmail(
    clientToken: string,
    email: string,
  ): Promise<UsersLookupByEmailResponse["user"]> {
    try {
      const { ok, user } = await this.client.users.lookupByEmail({
        token: clientToken,
        email,
      });
      if (!ok) throw new Error(`No slack user found with ${email}`);
  
      return user;
    } catch (error) {
      logger.error("SlackRepository", "findByEmail had an error",  error);
      throw error;
    }
  }
}

In this case, the situation is different: if an exception is raising within findByEmail's try block, the catch block will re-throw the exception. Therefore, any code calling findByEmail must either wrap the call location in a try/catch block or use Promise chains to catch the exception.

I will close this issue but if you have a better repro case, feel free to re-open.