Skip to content

Commit 1bbff2c

Browse files
authored
Tag errors by name (#1764)
1 parent d6f9f1e commit 1bbff2c

File tree

9 files changed

+210
-127
lines changed

9 files changed

+210
-127
lines changed

.changeset/rotten-owls-reply.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"livekit-client": patch
3+
---
4+
5+
Tag errors by name

src/api/SignalClient.test.ts

Lines changed: 12 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
DisconnectReason,
23
JoinResponse,
34
LeaveRequest,
45
ReconnectResponse,
@@ -177,9 +178,12 @@ describe('SignalClient.connect', () => {
177178
websocketTimeout: 100,
178179
};
179180

180-
await expect(
181-
signalClient.join('wss://test.livekit.io', 'test-token', shortTimeoutOptions),
182-
).rejects.toThrow(ConnectionError);
181+
const error = await signalClient
182+
.join('wss://test.livekit.io', 'test-token', shortTimeoutOptions)
183+
.catch((e) => e);
184+
185+
expect(error).toBeInstanceOf(ConnectionError);
186+
expect(error.reason).toBe(ConnectionErrorReason.Cancelled);
183187
});
184188
});
185189

@@ -333,11 +337,7 @@ describe('SignalClient.connect', () => {
333337
});
334338

335339
it('should handle ConnectionError from WebSocket rejection', async () => {
336-
const customError = new ConnectionError(
337-
'Custom error',
338-
ConnectionErrorReason.InternalError,
339-
500,
340-
);
340+
const customError = ConnectionError.internal('Custom error', { status: 500 });
341341

342342
mockWebSocketStream({
343343
opened: Promise.reject(customError),
@@ -393,11 +393,9 @@ describe('SignalClient.connect', () => {
393393
await expect(
394394
signalClient.join('wss://test.livekit.io', 'test-token', defaultOptions),
395395
).rejects.toMatchObject(
396-
new ConnectionError(
396+
ConnectionError.leaveRequest(
397397
'Received leave request while trying to (re)connect',
398-
ConnectionErrorReason.LeaveRequest,
399-
undefined,
400-
1,
398+
DisconnectReason.CLIENT_INITIATED,
401399
),
402400
);
403401
});
@@ -700,11 +698,7 @@ describe('SignalClient.handleConnectionError', () => {
700698
});
701699

702700
it('should return ConnectionError as-is if it is already a ConnectionError', async () => {
703-
const connectionError = new ConnectionError(
704-
'Custom error',
705-
ConnectionErrorReason.InternalError,
706-
500,
707-
);
701+
const connectionError = ConnectionError.internal('Custom error');
708702

709703
(global.fetch as any).mockResolvedValueOnce({
710704
status: 500,
@@ -737,7 +731,6 @@ describe('SignalClient.handleConnectionError', () => {
737731

738732
expect(result).toBeInstanceOf(ConnectionError);
739733
expect(result.reason).toBe(ConnectionErrorReason.InternalError);
740-
expect(result.status).toBe(500);
741734
}
742735
});
743736

@@ -755,7 +748,7 @@ describe('SignalClient.handleConnectionError', () => {
755748
});
756749

757750
it('should handle fetch throwing ConnectionError', async () => {
758-
const fetchError = new ConnectionError('Fetch failed', ConnectionErrorReason.ServerUnreachable);
751+
const fetchError = ConnectionError.serverUnreachable('Fetch failed');
759752
(global.fetch as any).mockRejectedValueOnce(fetchError);
760753

761754
const handleMethod = (signalClient as any).handleConnectionError;

src/api/SignalClient.ts

Lines changed: 13 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ import {
4545
protoInt64,
4646
} from '@livekit/protocol';
4747
import log, { LoggerNames, getLogger } from '../logger';
48-
import { ConnectionError, ConnectionErrorReason } from '../room/errors';
48+
import { ConnectionError } from '../room/errors';
4949
import CriticalTimers from '../room/timers';
5050
import type { LoggerOptions } from '../room/types';
5151
import { getClientInfo, isReactNative, sleep } from '../room/utils';
@@ -319,7 +319,7 @@ export class SignalClient {
319319
this.close();
320320
}
321321
cleanupAbortHandlers();
322-
reject(target instanceof AbortSignal ? target.reason : target);
322+
reject(ConnectionError.cancelled(reason));
323323
};
324324

325325
abortSignal?.addEventListener('abort', abortHandler);
@@ -330,12 +330,7 @@ export class SignalClient {
330330
};
331331

332332
const wsTimeout = setTimeout(() => {
333-
abortHandler(
334-
new ConnectionError(
335-
'room connection has timed out (signal)',
336-
ConnectionErrorReason.ServerUnreachable,
337-
),
338-
);
333+
abortHandler(ConnectionError.timeout('room connection has timed out (signal)'));
339334
}, opts.websocketTimeout);
340335

341336
const handleSignalConnected = (
@@ -364,9 +359,8 @@ export class SignalClient {
364359
.then((closeInfo) => {
365360
if (this.isEstablishingConnection) {
366361
reject(
367-
new ConnectionError(
362+
ConnectionError.internal(
368363
`Websocket got closed during a (re)connection attempt: ${closeInfo.reason}`,
369-
ConnectionErrorReason.InternalError,
370364
),
371365
);
372366
}
@@ -387,9 +381,8 @@ export class SignalClient {
387381
.catch((reason) => {
388382
if (this.isEstablishingConnection) {
389383
reject(
390-
new ConnectionError(
384+
ConnectionError.internal(
391385
`Websocket error during a (re)connection attempt: ${reason}`,
392-
ConnectionErrorReason.InternalError,
393386
),
394387
);
395388
}
@@ -416,10 +409,7 @@ export class SignalClient {
416409
const firstMessage = await signalReader.read();
417410
signalReader.releaseLock();
418411
if (!firstMessage.value) {
419-
throw new ConnectionError(
420-
'no message received as first message',
421-
ConnectionErrorReason.InternalError,
422-
);
412+
throw ConnectionError.internal('no message received as first message');
423413
}
424414

425415
const firstSignalResponse = parseSignalResponse(firstMessage.value);
@@ -971,27 +961,24 @@ export class SignalClient {
971961
} else if (this.isEstablishingConnection && firstSignalResponse.message?.case === 'leave') {
972962
return {
973963
isValid: false,
974-
error: new ConnectionError(
964+
error: ConnectionError.leaveRequest(
975965
'Received leave request while trying to (re)connect',
976-
ConnectionErrorReason.LeaveRequest,
977-
undefined,
978966
firstSignalResponse.message.value.reason,
979967
),
980968
};
981969
} else if (!isReconnect) {
982970
// non-reconnect case, should receive join response first
983971
return {
984972
isValid: false,
985-
error: new ConnectionError(
973+
error: ConnectionError.internal(
986974
`did not receive join response, got ${firstSignalResponse.message?.case} instead`,
987-
ConnectionErrorReason.InternalError,
988975
),
989976
};
990977
}
991978

992979
return {
993980
isValid: false,
994-
error: new ConnectionError('Unexpected first message', ConnectionErrorReason.InternalError),
981+
error: ConnectionError.internal('Unexpected first message'),
995982
};
996983
}
997984

@@ -1010,22 +997,20 @@ export class SignalClient {
1010997
const resp = await fetch(validateUrl);
1011998
if (resp.status.toFixed(0).startsWith('4')) {
1012999
const msg = await resp.text();
1013-
return new ConnectionError(msg, ConnectionErrorReason.NotAllowed, resp.status);
1000+
return ConnectionError.notAllowed(msg, resp.status);
10141001
} else if (reason instanceof ConnectionError) {
10151002
return reason;
10161003
} else {
1017-
return new ConnectionError(
1004+
return ConnectionError.internal(
10181005
`Encountered unknown websocket error during connection: ${reason}`,
1019-
ConnectionErrorReason.InternalError,
1020-
resp.status,
1006+
{ status: resp.status, statusText: resp.statusText },
10211007
);
10221008
}
10231009
} catch (e) {
10241010
return e instanceof ConnectionError
10251011
? e
1026-
: new ConnectionError(
1012+
: ConnectionError.serverUnreachable(
10271013
e instanceof Error ? e.message : 'server was not reachable',
1028-
ConnectionErrorReason.ServerUnreachable,
10291014
);
10301015
}
10311016
}

src/room/PCTransportManager.ts

Lines changed: 4 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { SignalTarget } from '@livekit/protocol';
33
import log, { LoggerNames, getLogger } from '../logger';
44
import PCTransport, { PCEvents } from './PCTransport';
55
import { roomConnectOptionDefaults } from './defaults';
6-
import { ConnectionError, ConnectionErrorReason } from './errors';
6+
import { ConnectionError } from './errors';
77
import CriticalTimers from './timers';
88
import type { LoggerOptions } from './types';
99
import { sleep } from './utils';
@@ -345,12 +345,7 @@ export class PCTransportManager {
345345
this.log.warn('abort transport connection', this.logContext);
346346
CriticalTimers.clearTimeout(connectTimeout);
347347

348-
reject(
349-
new ConnectionError(
350-
'room connection has been cancelled',
351-
ConnectionErrorReason.Cancelled,
352-
),
353-
);
348+
reject(ConnectionError.cancelled('room connection has been cancelled'));
354349
};
355350
if (abortController?.signal.aborted) {
356351
abortHandler();
@@ -359,23 +354,13 @@ export class PCTransportManager {
359354

360355
const connectTimeout = CriticalTimers.setTimeout(() => {
361356
abortController?.signal.removeEventListener('abort', abortHandler);
362-
reject(
363-
new ConnectionError(
364-
'could not establish pc connection',
365-
ConnectionErrorReason.InternalError,
366-
),
367-
);
357+
reject(ConnectionError.internal('could not establish pc connection'));
368358
}, timeout);
369359

370360
while (this.state !== PCTransportState.CONNECTED) {
371361
await sleep(50); // FIXME we shouldn't rely on `sleep` in the connection paths, as it invokes `setTimeout` which can be drastically throttled by browser implementations
372362
if (abortController?.signal.aborted) {
373-
reject(
374-
new ConnectionError(
375-
'room connection has been cancelled',
376-
ConnectionErrorReason.Cancelled,
377-
),
378-
);
363+
reject(ConnectionError.cancelled('room connection has been cancelled'));
379364
return;
380365
}
381366
}

src/room/RTCEngine.ts

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -381,10 +381,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
381381
const publicationTimeout = setTimeout(() => {
382382
delete this.pendingTrackResolvers[req.cid];
383383
reject(
384-
new ConnectionError(
385-
'publication of local track timed out, no response from server',
386-
ConnectionErrorReason.Timeout,
387-
),
384+
ConnectionError.timeout('publication of local track timed out, no response from server'),
388385
);
389386
}, 10_000);
390387
this.pendingTrackResolvers[req.cid] = {
@@ -1228,10 +1225,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
12281225
} catch (e: any) {
12291226
// TODO do we need a `failed` state here for the PC?
12301227
this.pcState = PCState.Disconnected;
1231-
throw new ConnectionError(
1232-
`could not establish PC connection, ${e.message}`,
1233-
ConnectionErrorReason.InternalError,
1234-
);
1228+
throw ConnectionError.internal(`could not establish PC connection, ${e.message}`);
12351229
}
12361230
}
12371231

@@ -1412,10 +1406,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
14121406
const transport = subscriber ? this.pcManager.subscriber : this.pcManager.publisher;
14131407
const transportName = subscriber ? 'Subscriber' : 'Publisher';
14141408
if (!transport) {
1415-
throw new ConnectionError(
1416-
`${transportName} connection not set`,
1417-
ConnectionErrorReason.InternalError,
1418-
);
1409+
throw ConnectionError.internal(`${transportName} connection not set`);
14191410
}
14201411

14211412
let needNegotiation = false;
@@ -1456,9 +1447,8 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
14561447
await sleep(50);
14571448
}
14581449

1459-
throw new ConnectionError(
1450+
throw ConnectionError.internal(
14601451
`could not establish ${transportName} connection, state: ${transport.getICEConnectionState()}`,
1461-
ConnectionErrorReason.InternalError,
14621452
);
14631453
}
14641454

src/room/RegionUrlProvider.test.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -180,8 +180,9 @@ describe('RegionUrlProvider', () => {
180180
const provider = new RegionUrlProvider('wss://test.livekit.cloud', 'token');
181181
fetchMock.mockResolvedValue(createMockResponse(401));
182182

183-
await expect(provider.fetchRegionSettings()).rejects.toThrow(ConnectionError);
184-
await expect(provider.fetchRegionSettings()).rejects.toMatchObject({
183+
const error = await provider.fetchRegionSettings().catch((e) => e);
184+
expect(error).toBeInstanceOf(ConnectionError);
185+
expect(error).toMatchObject({
185186
reason: ConnectionErrorReason.NotAllowed,
186187
status: 401,
187188
});
@@ -191,11 +192,9 @@ describe('RegionUrlProvider', () => {
191192
const provider = new RegionUrlProvider('wss://test.livekit.cloud', 'token');
192193
fetchMock.mockResolvedValue(createMockResponse(500));
193194

194-
await expect(provider.fetchRegionSettings()).rejects.toThrow(ConnectionError);
195-
await expect(provider.fetchRegionSettings()).rejects.toMatchObject({
196-
reason: ConnectionErrorReason.InternalError,
197-
status: 500,
198-
});
195+
const error = await provider.fetchRegionSettings().catch((e) => e);
196+
expect(error).toBeInstanceOf(ConnectionError);
197+
expect(error.reason).toBe(ConnectionErrorReason.InternalError);
199198
});
200199

201200
it('extracts max-age from Cache-Control header', async () => {
@@ -725,7 +724,7 @@ describe('RegionUrlProvider', () => {
725724

726725
expect(error).toBeInstanceOf(ConnectionError);
727726
expect(error.reason).toBe(ConnectionErrorReason.ServerUnreachable);
728-
expect(error.status).toBe(500);
727+
expect(error.status).toBe(undefined);
729728
expect(error.message).toContain('Failed to fetch');
730729
});
731730

src/room/RegionUrlProvider.ts

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -45,26 +45,27 @@ export class RegionUrlProvider {
4545
const regionSettings = (await regionSettingsResponse.json()) as RegionSettings;
4646
return { regionSettings, updatedAtInMs: Date.now(), maxAgeInMs };
4747
} else {
48-
throw new ConnectionError(
49-
`Could not fetch region settings: ${regionSettingsResponse.statusText}`,
50-
regionSettingsResponse.status === 401
51-
? ConnectionErrorReason.NotAllowed
52-
: ConnectionErrorReason.InternalError,
53-
regionSettingsResponse.status,
54-
);
48+
if (regionSettingsResponse.status === 401) {
49+
throw ConnectionError.notAllowed(
50+
`Could not fetch region settings: ${regionSettingsResponse.statusText}`,
51+
regionSettingsResponse.status,
52+
);
53+
} else {
54+
throw ConnectionError.internal(
55+
`Could not fetch region settings: ${regionSettingsResponse.statusText}`,
56+
);
57+
}
5558
}
5659
} catch (e: unknown) {
5760
if (e instanceof ConnectionError) {
5861
// rethrow connection errors
5962
throw e;
6063
} else if (signal?.aborted) {
61-
throw new ConnectionError(`Region fetching was aborted`, ConnectionErrorReason.Cancelled);
64+
throw ConnectionError.cancelled(`Region fetching was aborted`);
6265
} else {
63-
// wrap other errors as connection errors (e.g. timeouts)
64-
throw new ConnectionError(
66+
// wrap other errors as connection errors
67+
throw ConnectionError.serverUnreachable(
6568
`Could not fetch region settings, ${e instanceof Error ? `${e.name}: ${e.message}` : e}`,
66-
ConnectionErrorReason.ServerUnreachable,
67-
500, // using 500 as a catch-all manually set error code here
6869
);
6970
}
7071
} finally {

0 commit comments

Comments
 (0)