fix: retry ws opening-handshake timeout under the default ct policy (#1565) (#1576)

The ws v8 library throws a plain Error('Opening handshake has timed out') with no
.code and name 'Error' when the handshake timeout (JAMBONES_WS_HANDSHAKE_TIMEOUT_MS,
default 1500ms) elapses. BaseRequestor._shouldRetry classified retryable errors only
by .code/.name/.statusCode, so this error matched neither the ct nor rt bucket and was
never retried under the default rp=ct policy — the call failed immediately on the first
attempt, silently bypassing maxReconnects.

Match the handshake-timeout message in the ct bucket so a transient slow WS upgrade
(e.g. a serverless/edge backend cold start) is retried as intended. Adds tape coverage
for both the default-ct retry path and the rp=4xx no-retry path.
This commit is contained in:
Ben Younes
2026-09-13 11:49:40 +01:00
committed by GitHub
parent 1d1d16e221
commit 74510b0bef
2 changed files with 97 additions and 2 deletions
+9 -2
View File
@@ -6,6 +6,11 @@ const timeSeries = require('@jambonz/time-series');
const {NODE_ENV, JAMBONES_TIME_SERIES_HOST} = require('../config');
let alerter ;
// The ws (v8) library throws this plain Error on an opening-handshake timeout. It carries no
// .code and .name === 'Error', so it must be matched by message to be treated as a connection
// timeout (ct) for retry purposes (issue #1565).
const WS_HANDSHAKE_TIMEOUT_MESSAGE = 'Opening handshake has timed out';
class BaseRequestor extends Emitter {
constructor(logger, account_sid, hook, secret) {
super();
@@ -99,11 +104,13 @@ class BaseRequestor extends Emitter {
* @returns {boolean} True if the error should be retried
*/
_shouldRetry(err, rpValues) {
// ct = connection timeout (ECONNREFUSED, ETIMEDOUT, etc)
// ct = connection timeout (ECONNREFUSED, ETIMEDOUT, etc). The ws opening-handshake timeout
// has no .code, so match it by message so the default ct policy retries it (issue #1565).
const isCt = err.code === 'ECONNREFUSED' ||
err.code === 'ETIMEDOUT' ||
err.code === 'ECONNRESET' ||
err.code === 'ECONNABORTED';
err.code === 'ECONNABORTED' ||
err.message === WS_HANDSHAKE_TIMEOUT_MESSAGE;
// rt = request timeout
const isRt = err.name === 'TimeoutError';
// 4xx = client errors