// SPDX-FileCopyrightText: 2026 LiveKit, Inc. // // SPDX-License-Identifier: Apache-2.0 import { afterEach, describe, expect, it, vi } from 'vitest'; import { REQUEST_ID_HEADER, ServerError, SipCallError, TwirpRpc } from './TwirpRPC.js'; describe('SipCallError', () => { it('renders the SIP status, Twirp code, and extra metadata', () => { const err = SipCallError.fromServerError( new ServerError( 'Too Many Requests', 'twirp error: sip status 486', 429, 'failed_precondition', { sip_status_code: '486', sip_status: 'Busy Here', error_details: 'CAgS...base64...', region: 'us-east', }, ), ); expect(err).toBeInstanceOf(ServerError); expect(err.name).toBe('SipCallError'); expect(err.sipStatusCode).toBe(486); expect(err.sipStatus).toBe('Busy Here'); const printed = err.toString(); expect(printed).toContain('SipCallError'); expect(printed).toContain('486'); expect(printed).toContain('Busy Here'); expect(printed).toContain('failed_precondition'); expect(printed).toContain('region=us-east'); // other metadata is surfaced expect(printed).not.toContain('error_details'); // opaque blob is omitted }); it('falls back to the original message when there is no SIP status', () => { const err = SipCallError.fromServerError(new ServerError('Internal', 'boom', 500, 'internal')); expect(err.message).toBe('boom'); }); }); describe('request id', () => { afterEach(() => { vi.restoreAllMocks(); }); const okResponse = () => ({ ok: true, status: 200, json: async () => ({}) }) as unknown as Response; const errorResponse = (status: number) => ({ ok: false, status, statusText: 'Service Unavailable', headers: { get: () => null }, text: async () => 'unavailable', }) as unknown as Response; // The header lets the server dedup a request that the SDK replayed. it('stamps a request id on every call', async () => { const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(okResponse()); const rpc = new TwirpRpc('https://test.livekit.cloud', 'livekit', { failover: false }); await rpc.request('RoomService', 'CreateRoom', {}, {}); await rpc.request('RoomService', 'CreateRoom', {}, {}); const ids = fetchSpy.mock.calls.map( ([, init]) => (init!.headers as Record)[REQUEST_ID_HEADER], ); expect(ids[0]).toBeTruthy(); expect(ids[1]).toBeTruthy(); // A new logical call is a new request, so it gets its own id. expect(ids[0]).not.toBe(ids[1]); }); // The id is generated once per logical call, so every failover attempt must // carry the same value. it('keeps the same request id across failover attempts', async () => { let attempt = 0; const fetchSpy = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { // Region discovery, not a replay of the request itself. if (`${input}`.endsWith('/settings/regions')) { return { ok: true, status: 200, headers: { get: () => 'max-age=0' }, json: async () => ({ regions: [ { url: 'https://r1.retryid.livekit.cloud' }, { url: 'https://r2.retryid.livekit.cloud' }, ], }), } as unknown as Response; } attempt += 1; return attempt < 3 ? errorResponse(503) : okResponse(); }); const rpc = new TwirpRpc('https://primary.retryid.livekit.cloud', 'livekit', { failoverBackoffMs: 0, }); await rpc.request('RoomService', 'CreateRoom', {}, {}); const ids = fetchSpy.mock.calls .filter(([input]) => !`${input}`.endsWith('/settings/regions')) .map(([, init]) => (init!.headers as Record)[REQUEST_ID_HEADER]); expect(ids).toHaveLength(3); expect(ids[0]).toBeTruthy(); expect(new Set(ids).size).toBe(1); }); });