Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/socket-mode/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@slack/socket-mode",
"version": "3.0.0-rc.1",
"version": "3.0.0-rc.2",
"description": "Official library for using the Slack Platform's Socket Mode API",
"author": "Slack Technologies, LLC",
"license": "MIT",
Expand Down
4 changes: 2 additions & 2 deletions packages/socket-mode/src/SlackWebSocket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { channel } from 'node:diagnostics_channel';
import type { EventEmitter } from 'eventemitter3';
import { CloseEvent, type Dispatcher, ErrorEvent, MessageEvent, ping, WebSocket } from 'undici';

import { websocketErrorWithOriginal } from './errors';
import { SMWebsocketError } from './errors';
import log, { type Logger, LogLevel } from './logger';
import type { SocketModeDispatcher } from './SocketModeOptions';

Expand Down Expand Up @@ -151,7 +151,7 @@ export class SlackWebSocket {
}
this.logger.error(`WebSocket error occurred: ${event.message}`);
this.disconnect();
this.options.client.emit('error', websocketErrorWithOriginal(event.error ?? new Error(event.message)));
this.options.client.emit('error', new SMWebsocketError(event.error ?? new Error(event.message)));
};
this.websocket.addEventListener('error', this.errorHandler);

Expand Down
22 changes: 11 additions & 11 deletions packages/socket-mode/src/SocketModeClient.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import {
ErrorCode as APICallErrorCode,
type AppsConnectionsOpenResponse,
addAppMetadata,
type WebAPICallError,
WebAPIHTTPError,
WebAPIPlatformError,
WebAPIRequestError,
WebClient,
type WebClientOptions,
} from '@slack/web-api';
Expand All @@ -11,7 +12,7 @@ import { EventEmitter } from 'eventemitter3';
import { type RequestInit, fetch as undiciFetch } from 'undici';

import packageJson from '../package.json';
import { sendWhileDisconnectedError, sendWhileNotReadyError, websocketErrorWithOriginal } from './errors';
import { SMSendWhileDisconnectedError, SMSendWhileNotReadyError, SMWebsocketError } from './errors';
import log, { type Logger, LogLevel } from './logger';
import { SlackWebSocket, WS_READY_STATES } from './SlackWebSocket';
import type { SocketModeDispatcher, SocketModeOptions } from './SocketModeOptions';
Expand Down Expand Up @@ -279,16 +280,15 @@ export class SocketModeClient extends EventEmitter {
} catch (error) {
// TODO: Python catches rate limit errors when interacting with this API: https://github.com/slackapi/python-slack-sdk/blob/main/slack_sdk/socket_mode/client.py#L51
this.logger.error(`Failed to retrieve a new WSS URL (error: ${error})`);
const err = error as WebAPICallError;
let isRecoverable = true;
if (
err.code === APICallErrorCode.PlatformError &&
(Object.values(UnrecoverableSocketModeStartError) as string[]).includes(err.data.error)
error instanceof WebAPIPlatformError &&
(Object.values(UnrecoverableSocketModeStartError) as string[]).includes(error.data.error)
) {
isRecoverable = false;
} else if (err.code === APICallErrorCode.RequestError) {
} else if (error instanceof WebAPIRequestError) {
isRecoverable = false;
} else if (err.code === APICallErrorCode.HTTPError) {
} else if (error instanceof WebAPIHTTPError) {
isRecoverable = false;
}
if (this.autoReconnectEnabled && isRecoverable) {
Expand Down Expand Up @@ -410,10 +410,10 @@ export class SocketModeClient extends EventEmitter {
);
if (this.websocket === undefined) {
this.logger.error('Failed to send a message as the client is not connected');
reject(sendWhileDisconnectedError());
reject(new SMSendWhileDisconnectedError());
} else if (!this.websocket.isActive()) {
this.logger.error('Failed to send a message as the client has no active connection');
reject(sendWhileNotReadyError());
reject(new SMSendWhileNotReadyError());
} else {
this.emit('outgoing_message', message);

Expand All @@ -422,7 +422,7 @@ export class SocketModeClient extends EventEmitter {
this.websocket.send(flatMessage, (error) => {
if (error) {
this.logger.error(`Failed to send a WebSocket message (error: ${error})`);
return reject(websocketErrorWithOriginal(error));
return reject(new SMWebsocketError(error));
}
return resolve();
});
Expand Down
73 changes: 73 additions & 0 deletions packages/socket-mode/src/errors.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import assert from 'node:assert/strict';
import { describe, it } from 'node:test';

import {
ErrorCode,
SMNoReplyReceivedError,
SMPlatformError,
SMSendWhileDisconnectedError,
SMSendWhileNotReadyError,
SMWebsocketError,
} from './errors';

describe('error classes', () => {
describe('SMWebsocketError', () => {
it('should be an instance of Error and SMWebsocketError', () => {
const original = new Error('connection reset');
const err = new SMWebsocketError(original);
assert.ok(err instanceof Error);
assert.ok(err instanceof SMWebsocketError);
assert.equal(err.code, ErrorCode.WebsocketError);
assert.equal(err.original, original);
assert.equal(err.cause, original);
assert.equal(err.message, 'connection reset');
assert.equal(err.name, 'SMWebsocketError');
});
});

describe('SMPlatformError', () => {
it('should be an instance of Error and SMPlatformError', () => {
const event = { error: { msg: 'not_authed' } };
const err = new SMPlatformError(event);
assert.ok(err instanceof Error);
assert.ok(err instanceof SMPlatformError);
assert.equal(err.code, ErrorCode.SendMessagePlatformError);
assert.equal(err.data, event);
assert.equal(err.message, 'An API error occurred: not_authed');
assert.equal(err.name, 'SMPlatformError');
});
});

describe('SMNoReplyReceivedError', () => {
it('should be an instance of Error and SMNoReplyReceivedError', () => {
const err = new SMNoReplyReceivedError();
assert.ok(err instanceof Error);
assert.ok(err instanceof SMNoReplyReceivedError);
assert.equal(err.code, ErrorCode.NoReplyReceivedError);
assert.equal(err.name, 'SMNoReplyReceivedError');
assert.ok(err.message.includes('no server acknowledgement'));
});
});

describe('SMSendWhileDisconnectedError', () => {
it('should be an instance of Error and SMSendWhileDisconnectedError', () => {
const err = new SMSendWhileDisconnectedError();
assert.ok(err instanceof Error);
assert.ok(err instanceof SMSendWhileDisconnectedError);
assert.equal(err.code, ErrorCode.SendWhileDisconnectedError);
assert.equal(err.name, 'SMSendWhileDisconnectedError');
assert.ok(err.message.includes('not connected'));
});
});

describe('SMSendWhileNotReadyError', () => {
it('should be an instance of Error and SMSendWhileNotReadyError', () => {
const err = new SMSendWhileNotReadyError();
assert.ok(err instanceof Error);
assert.ok(err instanceof SMSendWhileNotReadyError);
assert.equal(err.code, ErrorCode.SendWhileNotReadyError);
assert.equal(err.name, 'SMSendWhileNotReadyError');
assert.ok(err.message.includes('not ready'));
});
});
});
120 changes: 44 additions & 76 deletions packages/socket-mode/src/errors.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* All errors produced by this package adhere to this interface
* @deprecated Use `instanceof` checks with specific error classes (e.g. `SMWebsocketError`) instead.
*/
export interface CodedError extends Error {
code: string;
Expand All @@ -24,93 +24,61 @@ export type SMCallError =
| SMSendWhileDisconnectedError
| SMSendWhileNotReadyError;

export interface SMPlatformError extends CodedError {
code: ErrorCode.SendMessagePlatformError;
export class SMPlatformError extends Error {
readonly code = ErrorCode.SendMessagePlatformError;
// biome-ignore lint/suspicious/noExplicitAny: errors can be anything
data: any;
}
readonly data: any;

export interface SMWebsocketError extends CodedError {
code: ErrorCode.WebsocketError;
original: Error;
// biome-ignore lint/suspicious/noExplicitAny: errors can be anything
constructor(event: any & { error: { msg: string } }) {
super(`An API error occurred: ${event.error.msg}`);
this.name = 'SMPlatformError';
Object.setPrototypeOf(this, new.target.prototype);
this.data = event;
}
}

export interface SMNoReplyReceivedError extends CodedError {
code: ErrorCode.NoReplyReceivedError;
}
export class SMWebsocketError extends Error {
readonly code = ErrorCode.WebsocketError;
readonly original: Error;

export interface SMSendWhileDisconnectedError extends CodedError {
code: ErrorCode.SendWhileDisconnectedError;
constructor(original: Error) {
super(original.message, { cause: original });
this.name = 'SMWebsocketError';
Object.setPrototypeOf(this, new.target.prototype);
this.original = original;
}
}

export interface SMSendWhileNotReadyError extends CodedError {
code: ErrorCode.SendWhileNotReadyError;
}
export class SMNoReplyReceivedError extends Error {
readonly code = ErrorCode.NoReplyReceivedError;

/**
* Factory for producing a {@link CodedError} from a generic error
*/
function errorWithCode(error: Error, code: ErrorCode): CodedError {
// NOTE: might be able to return something more specific than a CodedError with conditional typing
const codedError = error as Partial<CodedError>;
codedError.code = code;
return codedError as CodedError;
}

/**
* A factory to create SMWebsocketError objects.
*/
export function websocketErrorWithOriginal(original: Error): SMWebsocketError {
const error = errorWithCode(new Error(original.message), ErrorCode.WebsocketError) as Partial<SMWebsocketError>;
error.original = original;
return error as SMWebsocketError;
}

/**
* A factory to create SMPlatformError objects.
*/
export function platformErrorFromEvent(
// biome-ignore lint/suspicious/noExplicitAny: errors can be anything
event: any & { error: { msg: string } },
): SMPlatformError {
const error = errorWithCode(
new Error(`An API error occurred: ${event.error.msg}`),
ErrorCode.SendMessagePlatformError,
) as Partial<SMPlatformError>;
error.data = event;
return error as SMPlatformError;
}

// TODO: Is the below factory needed still?
/**
* A factory to create SMNoReplyReceivedError objects.
*/
export function noReplyReceivedError(): SMNoReplyReceivedError {
return errorWithCode(
new Error(
constructor() {
super(
'Message sent but no server acknowledgement was received. This may be caused by the client ' +
'changing connection state rather than any issue with the specific message. Check before resending.',
),
ErrorCode.NoReplyReceivedError,
) as SMNoReplyReceivedError;
);
this.name = 'SMNoReplyReceivedError';
Object.setPrototypeOf(this, new.target.prototype);
}
}

/**
* A factory to create SMSendWhileDisconnectedError objects.
*/
export function sendWhileDisconnectedError(): SMSendWhileDisconnectedError {
return errorWithCode(
new Error('Failed to send a WebSocket message as the client is not connected'),
ErrorCode.NoReplyReceivedError,
) as SMSendWhileDisconnectedError;
export class SMSendWhileDisconnectedError extends Error {
readonly code = ErrorCode.SendWhileDisconnectedError;

constructor() {
super('Failed to send a WebSocket message as the client is not connected');
this.name = 'SMSendWhileDisconnectedError';
Object.setPrototypeOf(this, new.target.prototype);
}
}

/**
* A factory to create SMSendWhileNotReadyError objects.
*/
export function sendWhileNotReadyError(): SMSendWhileNotReadyError {
return errorWithCode(
new Error('Failed to send a WebSocket message as the client is not ready'),
ErrorCode.NoReplyReceivedError,
) as SMSendWhileNotReadyError;
export class SMSendWhileNotReadyError extends Error {
readonly code = ErrorCode.SendWhileNotReadyError;

constructor() {
super('Failed to send a WebSocket message as the client is not ready');
this.name = 'SMSendWhileNotReadyError';
Object.setPrototypeOf(this, new.target.prototype);
}
}
Loading