Skip to content

Commit ec048c7

Browse files
authored
Apply server supplied cache control settings for region url provider (#1669)
1 parent beed33b commit ec048c7

File tree

4 files changed

+140
-4
lines changed

4 files changed

+140
-4
lines changed

.changeset/afraid-yaks-teach.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+
Apply server supplied cache control settings for region url provider

src/room/RegionUrlProvider.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { RegionInfo, RegionSettings } from '@livekit/protocol';
22
import log from '../logger';
33
import { ConnectionError, ConnectionErrorReason } from './errors';
4-
import { isCloud } from './utils';
4+
import { extractMaxAgeFromRequestHeaders, isCloud } from './utils';
55

66
export class RegionUrlProvider {
77
private serverUrl: URL;
@@ -12,7 +12,7 @@ export class RegionUrlProvider {
1212

1313
private lastUpdateAt: number = 0;
1414

15-
private settingsCacheTime = 3_000;
15+
private settingsCacheTimeInMs = 3_000;
1616

1717
private attemptedRegions: RegionInfo[] = [];
1818

@@ -37,7 +37,7 @@ export class RegionUrlProvider {
3737
if (!this.isCloud()) {
3838
throw Error('region availability is only supported for LiveKit Cloud domains');
3939
}
40-
if (!this.regionSettings || Date.now() - this.lastUpdateAt > this.settingsCacheTime) {
40+
if (!this.regionSettings || Date.now() - this.lastUpdateAt > this.settingsCacheTimeInMs) {
4141
this.regionSettings = await this.fetchRegionSettings(abortSignal);
4242
}
4343
const regionsLeft = this.regionSettings.regions.filter(
@@ -64,6 +64,11 @@ export class RegionUrlProvider {
6464
signal,
6565
});
6666
if (regionSettingsResponse.ok) {
67+
const maxAge = extractMaxAgeFromRequestHeaders(regionSettingsResponse.headers);
68+
if (maxAge) {
69+
log.debug(`setting local region settings cache time to ${maxAge} seconds`);
70+
this.settingsCacheTimeInMs = maxAge * 1000;
71+
}
6772
const regionSettings = (await regionSettingsResponse.json()) as RegionSettings;
6873
this.lastUpdateAt = Date.now();
6974
return regionSettings;

src/room/utils.test.ts

Lines changed: 116 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, expect, it } from 'vitest';
2-
import { splitUtf8, toWebsocketUrl } from './utils';
2+
import { extractMaxAgeFromRequestHeaders, splitUtf8, toWebsocketUrl } from './utils';
33

44
describe('toWebsocketUrl', () => {
55
it('leaves wss urls alone', () => {
@@ -60,3 +60,118 @@ describe('splitUtf8', () => {
6060
expect(splitUtf8('', 5)).toEqual([]);
6161
});
6262
});
63+
64+
describe('extractMaxAgeFromRequestHeaders', () => {
65+
it('extracts max-age from valid Cache-Control header', () => {
66+
const headers = new Headers();
67+
headers.set('Cache-Control', 'max-age=3600');
68+
expect(extractMaxAgeFromRequestHeaders(headers)).toBe(3600);
69+
});
70+
71+
it('extracts max-age from Cache-Control header with multiple directives', () => {
72+
const headers = new Headers();
73+
headers.set('Cache-Control', 'public, max-age=7200, must-revalidate');
74+
expect(extractMaxAgeFromRequestHeaders(headers)).toBe(7200);
75+
});
76+
77+
it('extracts max-age when it appears at the beginning', () => {
78+
const headers = new Headers();
79+
headers.set('Cache-Control', 'max-age=1800, public, no-cache');
80+
expect(extractMaxAgeFromRequestHeaders(headers)).toBe(1800);
81+
});
82+
83+
it('extracts max-age when it appears in the middle', () => {
84+
const headers = new Headers();
85+
headers.set('Cache-Control', 'public, max-age=900, no-store');
86+
expect(extractMaxAgeFromRequestHeaders(headers)).toBe(900);
87+
});
88+
89+
it('handles max-age with value 0', () => {
90+
const headers = new Headers();
91+
headers.set('Cache-Control', 'max-age=0');
92+
expect(extractMaxAgeFromRequestHeaders(headers)).toBe(0);
93+
});
94+
95+
it('handles large max-age values', () => {
96+
const headers = new Headers();
97+
headers.set('Cache-Control', 'max-age=31536000'); // 1 year
98+
expect(extractMaxAgeFromRequestHeaders(headers)).toBe(31536000);
99+
});
100+
101+
it('returns undefined when Cache-Control header is missing', () => {
102+
const headers = new Headers();
103+
expect(extractMaxAgeFromRequestHeaders(headers)).toBeUndefined();
104+
});
105+
106+
it('returns undefined when Cache-Control header exists but has no max-age', () => {
107+
const headers = new Headers();
108+
headers.set('Cache-Control', 'public, no-cache, must-revalidate');
109+
expect(extractMaxAgeFromRequestHeaders(headers)).toBeUndefined();
110+
});
111+
112+
it('returns undefined when Cache-Control header is empty', () => {
113+
const headers = new Headers();
114+
headers.set('Cache-Control', '');
115+
expect(extractMaxAgeFromRequestHeaders(headers)).toBeUndefined();
116+
});
117+
118+
it('handles Cache-Control header with extra whitespace', () => {
119+
const headers = new Headers();
120+
headers.set('Cache-Control', ' public, max-age=1200 , no-cache ');
121+
expect(extractMaxAgeFromRequestHeaders(headers)).toBe(1200);
122+
});
123+
124+
it('returns undefined for malformed max-age values', () => {
125+
const headers = new Headers();
126+
headers.set('Cache-Control', 'max-age=abc, public');
127+
expect(extractMaxAgeFromRequestHeaders(headers)).toBeUndefined();
128+
});
129+
130+
it('handles max-age with leading zeros', () => {
131+
const headers = new Headers();
132+
headers.set('Cache-Control', 'max-age=0003600');
133+
expect(extractMaxAgeFromRequestHeaders(headers)).toBe(3600);
134+
});
135+
136+
it('takes the first max-age value when multiple are present', () => {
137+
const headers = new Headers();
138+
headers.set('Cache-Control', 'max-age=1800, public, max-age=3600');
139+
expect(extractMaxAgeFromRequestHeaders(headers)).toBe(1800);
140+
});
141+
142+
it('handles case-insensitive Cache-Control header name', () => {
143+
const headers = new Headers();
144+
headers.set('cache-control', 'max-age=2400');
145+
expect(extractMaxAgeFromRequestHeaders(headers)).toBe(2400);
146+
});
147+
148+
it('handles Cache-Control with s-maxage (should ignore s-maxage)', () => {
149+
const headers = new Headers();
150+
headers.set('Cache-Control', 's-maxage=1800, max-age=3600');
151+
expect(extractMaxAgeFromRequestHeaders(headers)).toBe(3600);
152+
});
153+
154+
it('returns undefined when only s-maxage is present (no max-age)', () => {
155+
const headers = new Headers();
156+
headers.set('Cache-Control', 's-maxage=1800, public');
157+
expect(extractMaxAgeFromRequestHeaders(headers)).toBeUndefined();
158+
});
159+
160+
it('returns undefined for negative max-age values (regex only matches positive digits)', () => {
161+
const headers = new Headers();
162+
headers.set('Cache-Control', 'max-age=-100');
163+
expect(extractMaxAgeFromRequestHeaders(headers)).toBeUndefined();
164+
});
165+
166+
it('ignores non standard cache control custom-max-age values', () => {
167+
const headers = new Headers();
168+
headers.set('Cache-Control', 'custom-max-age=7200');
169+
expect(extractMaxAgeFromRequestHeaders(headers)).toBeUndefined();
170+
});
171+
172+
it('still works with comma separated non standard cache control custom-max-age values', () => {
173+
const headers = new Headers();
174+
headers.set('Cache-Control', 'custom-max-age=7200,max-age=3600');
175+
expect(extractMaxAgeFromRequestHeaders(headers)).toBe(3600);
176+
});
177+
});

src/room/utils.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -750,3 +750,14 @@ export function splitUtf8(s: string, n: number): Uint8Array[] {
750750
}
751751
return result;
752752
}
753+
754+
export function extractMaxAgeFromRequestHeaders(headers: Headers): number | undefined {
755+
const cacheControl = headers.get('Cache-Control');
756+
if (cacheControl) {
757+
const maxAge = cacheControl.match(/(?:^|[,\s])max-age=(\d+)/)?.[1];
758+
if (maxAge) {
759+
return parseInt(maxAge, 10);
760+
}
761+
}
762+
return undefined;
763+
}

0 commit comments

Comments
 (0)