Skip to content

Commit

Permalink
src: Add proper error handling (#283)
Browse files Browse the repository at this point in the history
Feature:

    Please see `doc/usage.md` for more information on error-handling.
  • Loading branch information
passion-27 committed Feb 9, 2017
1 parent 4572b5d commit 5f1a177
Show file tree
Hide file tree
Showing 8 changed files with 272 additions and 95 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Added:

1. Add methods:
* *TelegramBot#removeReplyListener()* (by @githugger)
1. Add proper error handling (by @GochoMugo)
1. Add health-check endpoint (by @mironov)
* `options.webHook.healthEndpoint`
1. Use *String#indexOf()*, instead of *RegExp#test()*, to
Expand Down
60 changes: 60 additions & 0 deletions doc/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
1. [Events](#events)
1. [WebHooks](#WebHooks)
1. [Sending files](#sending-files)
1. [Error handling](#error-handling)


* * *
Expand All @@ -29,6 +30,8 @@
1. `edited_channel_post`: Received a new version of a channel post that is known to the bot and was edited
1. `edited_channel_post_text`
1. `edited_channel_post_caption`
1. `polling_error`: Error occurred during polling. See [polling errors](#polling-errors).
1. `webhook_error`: Error occurred handling a webhook request. See [webhook errors](#webhook-errors).

**Tip:** Its much better to listen a specific event rather than on
`message` in order to stay safe from the content.
Expand Down Expand Up @@ -145,3 +148,60 @@ const bot = new TelegramBot(token, {
filepath: false,
});
```


<a name="error-handling"></a>
## Error handling

Every `Error` object we pass back has the properties:

* `code` (String):
* value is `EFATAL` if error was fatal e.g. network error
* value is `EPARSE` if response body could **not** be parsed
* value is `ETELEGRAM` if error was returned from Telegram servers
* `response` ([http.IncomingMessage](https://nodejs.org/api/http.html#http_class_http_incomingmessage)):
* available if `error.code` is **not** `EFATAL`
* `response.body` (String|Object): Error response from Telegram
* type is `String` if `error.code` is `EPARSE`
* type is `Object` if `error.code` is `ETELEGRAM`

For example, sending message to a non-existent user:

```js
bot.sendMessage(nonExistentUserId, 'text').catch(error => {
console.log(error.code); // => 'ETELEGRAM'
console.log(error.response.body); // => { ok: false, error_code: 400, description: 'Bad Request: chat not found' }
});
```

<a name="polling-errors"></a>
#### Polling errors

An error may occur during polling. It is up to you to handle it
as you see fit. You may decide to crash your bot after a maximum number
of polling errors occurring. **It is all up to you.**

By default, the polling error is just logged to stderr, if you do
**not** handle this event yourself.

Listen on the `'polling_error'` event. For example,

```js
bot.on('polling_error', (error) => {
console.log(error.code); // => 'EFATAL'
});
```

<a name="webhook-errors"></a>
#### WebHook errors

Just like with [polling errors](#polling-errors), you decide on how to
handle it. By default, the error is logged to stderr.

Listen on the `'webhook_error'` event. For example,

```js
bot.on('webhook_error', (error) => {
console.log(error.code); // => 'EPARSE'
});
```
59 changes: 59 additions & 0 deletions src/errors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
exports.BaseError = class BaseError extends Error {
/**
* @class BaseError
* @constructor
* @private
* @param {String} code Error code
* @param {String} message Error message
*/
constructor(code, message) {
super(`${code}: ${message}`);
this.code = code;
}
};


exports.FatalError = class FatalError extends exports.BaseError {
/**
* Fatal Error. Error code is `"EFATAL"`.
* @class FatalError
* @constructor
* @param {String|Error} data Error object or message
*/
constructor(data) {
const error = (typeof data === 'string') ? null : data;
const message = error ? error.message : data;
super('EFATAL', message);
if (error) this.stack = error.stack;
}
};


exports.ParseError = class ParseError extends exports.BaseError {
/**
* Error during parsing. Error code is `"EPARSE"`.
* @class ParseError
* @constructor
* @param {String} message Error message
* @param {http.IncomingMessage} response Server response
*/
constructor(message, response) {
super('EPARSE', message);
this.response = response;
}
};


exports.TelegramError = class TelegramError extends exports.BaseError {
/**
* Error returned from Telegram. Error code is `"ETELEGRAM"`.
* @class TelegramError
* @constructor
* @param {String} message Error message
* @param {http.IncomingMessage} response Server response
*/
constructor(message, response) {
super('ETELEGRAM', message);
this.response = response;
}
};
48 changes: 19 additions & 29 deletions src/telegram.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// shims
require('array.prototype.findindex').shim(); // for Node.js v0.x

const errors = require('./errors');
const TelegramBotWebHook = require('./telegramWebHook');
const TelegramBotPolling = require('./telegramPolling');
const debug = require('debug')('node-telegram-bot-api');
Expand Down Expand Up @@ -31,6 +32,10 @@ Promise.config({

class TelegramBot extends EventEmitter {

static get errors() {
return errors;
}

static get messageTypes() {
return _messageTypes;
}
Expand Down Expand Up @@ -136,7 +141,7 @@ class TelegramBot extends EventEmitter {
*/
_request(_path, options = {}) {
if (!this.token) {
throw new Error('Telegram Bot Token not provided!');
return Promise.reject(new errors.FatalError('Telegram Bot Token not provided!'));
}

if (this.options.request) {
Expand All @@ -158,30 +163,22 @@ class TelegramBot extends EventEmitter {
debug('HTTP request: %j', options);
return request(options)
.then(resp => {
if (resp.statusCode !== 200) {
const error = new Error(`${resp.statusCode} ${resp.body}`);
error.response = resp;
throw error;
}

let data;

try {
data = JSON.parse(resp.body);
data = resp.body = JSON.parse(resp.body);
} catch (err) {
const error = new Error(`Error parsing Telegram response: ${resp.body}`);
error.response = resp;
throw error;
throw new errors.ParseError(`Error parsing Telegram response: ${resp.body}`, resp);
}

if (data.ok) {
return data.result;
}

const error = new Error(`${data.error_code} ${data.description}`);
error.response = resp;
error.response.body = data;
throw error;
throw new errors.TelegramError(`${data.error_code} ${data.description}`, resp);
}).catch(error => {
// TODO: why can't we do `error instanceof errors.BaseError`?
if (error.response) throw error;
throw new errors.FatalError(error);
});
}

Expand Down Expand Up @@ -215,7 +212,7 @@ class TelegramBot extends EventEmitter {
} else if (Buffer.isBuffer(data)) {
const filetype = fileType(data);
if (!filetype) {
throw new Error('Unsupported Buffer file type');
throw new errors.FatalError('Unsupported Buffer file type');
}
formData = {};
formData[type] = {
Expand Down Expand Up @@ -256,11 +253,11 @@ class TelegramBot extends EventEmitter {
*/
startPolling(options = {}) {
if (this.hasOpenWebHook()) {
return Promise.reject(new Error('Polling and WebHook are mutually exclusive'));
return Promise.reject(new errors.FatalError('Polling and WebHook are mutually exclusive'));
}
options.restart = typeof options.restart === 'undefined' ? true : options.restart;
if (!this._polling) {
this._polling = new TelegramBotPolling(this._request.bind(this), this.options.polling, this.processUpdate.bind(this));
this._polling = new TelegramBotPolling(this);
}
return this._polling.start(options);
}
Expand Down Expand Up @@ -305,10 +302,10 @@ class TelegramBot extends EventEmitter {
*/
openWebHook() {
if (this.isPolling()) {
return Promise.reject(new Error('WebHook and Polling are mutually exclusive'));
return Promise.reject(new errors.FatalError('WebHook and Polling are mutually exclusive'));
}
if (!this._webHook) {
this._webHook = new TelegramBotWebHook(this.token, this.options.webHook, this.processUpdate.bind(this));
this._webHook = new TelegramBotWebHook(this);
}
return this._webHook.open();
}
Expand Down Expand Up @@ -385,14 +382,7 @@ class TelegramBot extends EventEmitter {
}
}

return this._request('setWebHook', opts)
.then(resp => {
if (!resp) {
throw new Error(resp);
}

return resp;
});
return this._request('setWebHook', opts);
}

/**
Expand Down
42 changes: 16 additions & 26 deletions src/telegramPolling.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,14 @@ const ANOTHER_WEB_HOOK_USED = 409;
class TelegramBotPolling {
/**
* Handles polling against the Telegram servers.
*
* @param {Function} request Function used to make HTTP requests
* @param {Boolean|Object} options Polling options
* @param {Number} [options.timeout=10] Timeout in seconds for long polling
* @param {Number} [options.interval=300] Interval between requests in milliseconds
* @param {Function} callback Function for processing a new update
* @see https://core.telegram.org/bots/api#getupdates
* @param {TelegramBot} bot
* @see https://core.telegram.org/bots/api#getting-updates
*/
constructor(request, options = {}, callback) {
/* eslint-disable no-param-reassign */
if (typeof options === 'function') {
callback = options;
options = {};
} else if (typeof options === 'boolean') {
options = {};
}
/* eslint-enable no-param-reassign */

this.request = request;
this.options = options;
this.options.timeout = (typeof options.timeout === 'number') ? options.timeout : 10;
this.options.interval = (typeof options.interval === 'number') ? options.interval : 300;
this.callback = callback;
constructor(bot) {
this.bot = bot;
this.options = (typeof bot.options.polling === 'boolean') ? {} : bot.options.polling;
this.options.timeout = (typeof this.options.timeout === 'number') ? this.options.timeout : 10;
this.options.interval = (typeof this.options.interval === 'number') ? this.options.interval : 300;
this._offset = 0;
this._lastUpdate = 0;
this._lastRequest = null;
Expand Down Expand Up @@ -102,13 +87,18 @@ class TelegramBotPolling {
updates.forEach(update => {
this._offset = update.update_id;
debug('updated offset: %s', this._offset);
this.callback(update);
this.bot.processUpdate(update);
});
return null;
})
.catch(err => {
debug('polling error: %s', err.message);
throw err;
if (this.bot.listeners('polling_error').length) {
this.bot.emit('polling_error', err);
} else {
console.error(err); // eslint-disable-line no-console
}
return null;
})
.finally(() => {
if (this._abort) {
Expand All @@ -128,7 +118,7 @@ class TelegramBotPolling {
* @private
*/
_unsetWebHook() {
return this.request('setWebHook');
return this.bot._request('setWebHook');
}

/**
Expand All @@ -144,7 +134,7 @@ class TelegramBotPolling {
};
debug('polling with options: %j', opts);

return this.request('getUpdates', opts)
return this.bot._request('getUpdates', opts)
.catch(err => {
if (err.response && err.response.statusCode === ANOTHER_WEB_HOOK_USED) {
return this._unsetWebHook();
Expand Down
Loading

0 comments on commit 5f1a177

Please sign in to comment.