Skip to content

Commit aa3c36b

Browse files
authored
Improve connection URL handling and add unit tests (#1468)
1 parent df39577 commit aa3c36b

5 files changed

Lines changed: 142 additions & 20 deletions

File tree

.changeset/honest-swans-switch.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+
Improve connection URL handling and add unit tests

pnpm-lock.yaml

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/api/SignalClient.ts

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,9 @@ import log, { LoggerNames, getLogger } from '../logger';
4343
import { ConnectionError, ConnectionErrorReason } from '../room/errors';
4444
import CriticalTimers from '../room/timers';
4545
import type { LoggerOptions } from '../room/types';
46-
import { getClientInfo, isReactNative, sleep, toWebsocketUrl } from '../room/utils';
46+
import { getClientInfo, isReactNative, sleep } from '../room/utils';
4747
import { AsyncQueue } from '../utils/AsyncQueue';
48+
import { createRtcUrl, createValidateUrl } from './utils';
4849

4950
// internal options
5051
interface ConnectOpts extends SignalOptions {
@@ -258,17 +259,10 @@ export class SignalClient {
258259
abortSignal?: AbortSignal,
259260
): Promise<JoinResponse | ReconnectResponse | undefined> {
260261
this.connectOptions = opts;
261-
const urlObj = new URL(toWebsocketUrl(url));
262-
// strip trailing slash
263-
const hasTrailingSlash = urlObj.pathname.endsWith('/');
264-
urlObj.pathname += hasTrailingSlash ? 'rtc' : '/rtc';
265-
266262
const clientInfo = getClientInfo();
267263
const params = createConnectionParams(token, clientInfo, opts);
268-
269-
for (const [key, value] of params) {
270-
urlObj.searchParams.set(key, value);
271-
}
264+
const rtcUrl = createRtcUrl(url, params);
265+
const validateUrl = createValidateUrl(rtcUrl);
272266

273267
return new Promise<JoinResponse | ReconnectResponse | undefined>(async (resolve, reject) => {
274268
const unlock = await this.connectionLock.lock();
@@ -298,7 +292,7 @@ export class SignalClient {
298292
abortHandler();
299293
}
300294
abortSignal?.addEventListener('abort', abortHandler);
301-
const redactedUrl = new URL(urlObj.toString());
295+
const redactedUrl = new URL(rtcUrl);
302296
if (redactedUrl.searchParams.has('access_token')) {
303297
redactedUrl.searchParams.set('access_token', '<redacted>');
304298
}
@@ -310,7 +304,7 @@ export class SignalClient {
310304
if (this.ws) {
311305
await this.close(false);
312306
}
313-
this.ws = new WebSocket(urlObj.toString());
307+
this.ws = new WebSocket(rtcUrl);
314308
this.ws.binaryType = 'arraybuffer';
315309

316310
this.ws.onopen = () => {
@@ -322,10 +316,7 @@ export class SignalClient {
322316
this.state = SignalConnectionState.DISCONNECTED;
323317
clearTimeout(wsTimeout);
324318
try {
325-
const validateURL = new URL(urlObj.toString());
326-
validateURL.protocol = `http${validateURL.protocol.substring(2)}`;
327-
validateURL.pathname += '/validate';
328-
const resp = await fetch(validateURL);
319+
const resp = await fetch(validateUrl);
329320
if (resp.status.toFixed(0).startsWith('4')) {
330321
const msg = await resp.text();
331322
reject(new ConnectionError(msg, ConnectionErrorReason.NotAllowed, resp.status));

src/api/utils.test.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { createRtcUrl, createValidateUrl } from './utils';
3+
4+
describe('createRtcUrl', () => {
5+
it('should create a basic RTC URL', () => {
6+
const url = 'wss://example.com';
7+
const searchParams = new URLSearchParams();
8+
const result = createRtcUrl(url, searchParams);
9+
expect(result.toString()).toBe('wss://example.com/rtc');
10+
});
11+
12+
it('should handle search parameters', () => {
13+
const url = 'wss://example.com';
14+
const searchParams = new URLSearchParams({
15+
token: 'test-token',
16+
room: 'test-room',
17+
});
18+
const result = createRtcUrl(url, searchParams);
19+
20+
const parsedResult = new URL(result);
21+
expect(parsedResult.pathname).toBe('/rtc');
22+
expect(parsedResult.searchParams.get('token')).toBe('test-token');
23+
expect(parsedResult.searchParams.get('room')).toBe('test-room');
24+
});
25+
26+
it('should handle ws protocol', () => {
27+
const url = 'ws://example.com';
28+
const searchParams = new URLSearchParams();
29+
const result = createRtcUrl(url, searchParams);
30+
31+
const parsedResult = new URL(result);
32+
expect(parsedResult.pathname).toBe('/rtc');
33+
});
34+
35+
it('should handle sub paths', () => {
36+
const url = 'wss://example.com/sub/path';
37+
const searchParams = new URLSearchParams();
38+
const result = createRtcUrl(url, searchParams);
39+
40+
const parsedResult = new URL(result);
41+
expect(parsedResult.pathname).toBe('/sub/path/rtc');
42+
});
43+
44+
it('should handle sub paths with trailing slashes', () => {
45+
const url = 'wss://example.com/sub/path/';
46+
const searchParams = new URLSearchParams();
47+
const result = createRtcUrl(url, searchParams);
48+
49+
const parsedResult = new URL(result);
50+
expect(parsedResult.pathname).toBe('/sub/path/rtc');
51+
});
52+
});
53+
54+
describe('createValidateUrl', () => {
55+
it('should create a basic validate URL', () => {
56+
const rtcUrl = createRtcUrl('wss://example.com', new URLSearchParams());
57+
const result = createValidateUrl(rtcUrl);
58+
expect(result.toString()).toBe('https://example.com/rtc/validate');
59+
});
60+
61+
it('should handle search parameters', () => {
62+
const rtcUrl = createRtcUrl(
63+
'wss://example.com',
64+
new URLSearchParams({
65+
token: 'test-token',
66+
room: 'test-room',
67+
}),
68+
);
69+
const result = createValidateUrl(rtcUrl);
70+
71+
const parsedResult = new URL(result);
72+
expect(parsedResult.pathname).toBe('/rtc/validate');
73+
expect(parsedResult.searchParams.get('token')).toBe('test-token');
74+
expect(parsedResult.searchParams.get('room')).toBe('test-room');
75+
});
76+
77+
it('should handle ws protocol', () => {
78+
const rtcUrl = createRtcUrl('ws://example.com', new URLSearchParams());
79+
const result = createValidateUrl(rtcUrl);
80+
81+
const parsedResult = new URL(result);
82+
expect(parsedResult.pathname).toBe('/rtc/validate');
83+
});
84+
85+
it('should preserve the original path', () => {
86+
const rtcUrl = createRtcUrl('wss://example.com/some/path', new URLSearchParams());
87+
const result = createValidateUrl(rtcUrl);
88+
89+
const parsedResult = new URL(result);
90+
expect(parsedResult.pathname).toBe('/some/path/rtc/validate');
91+
});
92+
93+
it('should handle sub paths with trailing slashes', () => {
94+
const rtcUrl = createRtcUrl('wss://example.com/sub/path/', new URLSearchParams());
95+
const result = createValidateUrl(rtcUrl);
96+
97+
const parsedResult = new URL(result);
98+
expect(parsedResult.pathname).toBe('/sub/path/rtc/validate');
99+
});
100+
});

src/api/utils.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { toHttpUrl } from '../room/utils';
2+
3+
export function createRtcUrl(url: string, searchParams: URLSearchParams) {
4+
const urlObj = new URL(url);
5+
searchParams.forEach((value, key) => {
6+
urlObj.searchParams.set(key, value);
7+
});
8+
return appendUrlPath(urlObj, 'rtc');
9+
}
10+
11+
export function createValidateUrl(rtcWsUrl: string) {
12+
const urlObj = new URL(toHttpUrl(rtcWsUrl));
13+
return appendUrlPath(urlObj, 'validate');
14+
}
15+
16+
function ensureTrailingSlash(url: string) {
17+
return url.endsWith('/') ? url : `${url}/`;
18+
}
19+
20+
function appendUrlPath(urlObj: URL, path: string) {
21+
const result = `${urlObj.protocol}//${urlObj.host}${ensureTrailingSlash(urlObj.pathname)}${path}`;
22+
if (urlObj.searchParams.size > 0) {
23+
return `${result}?${urlObj.searchParams.toString()}`;
24+
}
25+
return result;
26+
}

0 commit comments

Comments
 (0)