mirror of
https://github.com/vector-im/element-call.git
synced 2026-10-16 23:45:50 +00:00
Fall back to get_token as subscriber on widget API errors
This commit is contained in:
+30
-17
@@ -5,12 +5,21 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|||||||
Please see LICENSE in the repository root for full details.
|
Please see LICENSE in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { type MatrixClient } from "matrix-js-sdk";
|
import { ConnectionError, HTTPError, type MatrixClient } from "matrix-js-sdk";
|
||||||
import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager";
|
import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager";
|
||||||
|
|
||||||
import { extractFullConfigFromToken, type SFUConfig } from "./types.ts";
|
import { extractFullConfigFromToken, type SFUConfig } from "./types.ts";
|
||||||
import { doNetworkOperationWithRetry } from "../../utils/matrix.ts";
|
import { doNetworkOperationWithRetry } from "../../utils/matrix.ts";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In widget mode the host may refuse an action before any request is made
|
||||||
|
* which surfaces as a plain error. Asking again cannot change its answer,
|
||||||
|
* so only errors from the network are worth retrying.
|
||||||
|
*/
|
||||||
|
function isNetworkError(e: unknown): boolean {
|
||||||
|
return e instanceof HTTPError || e instanceof ConnectionError;
|
||||||
|
}
|
||||||
|
|
||||||
export type ClientGetTokenParts = Pick<
|
export type ClientGetTokenParts = Pick<
|
||||||
MatrixClient,
|
MatrixClient,
|
||||||
"_unstable_getLivekitToken"
|
"_unstable_getLivekitToken"
|
||||||
@@ -55,14 +64,16 @@ export async function getSFUConfig({
|
|||||||
roomId,
|
roomId,
|
||||||
slotId,
|
slotId,
|
||||||
}: GetSFUConfigParams): Promise<SFUConfig> {
|
}: GetSFUConfigParams): Promise<SFUConfig> {
|
||||||
const res = await doNetworkOperationWithRetry(async () =>
|
const res = await doNetworkOperationWithRetry(
|
||||||
client._unstable_getLivekitToken({
|
async () =>
|
||||||
url,
|
client._unstable_getLivekitToken({
|
||||||
server_name: serverName,
|
url,
|
||||||
room_id: roomId,
|
server_name: serverName,
|
||||||
slot_id: slotId,
|
room_id: roomId,
|
||||||
member_id: membership.memberId,
|
slot_id: slotId,
|
||||||
}),
|
member_id: membership.memberId,
|
||||||
|
}),
|
||||||
|
isNetworkError,
|
||||||
);
|
);
|
||||||
return extractFullConfigFromToken({ url, jwt: res.jwt });
|
return extractFullConfigFromToken({ url, jwt: res.jwt });
|
||||||
}
|
}
|
||||||
@@ -112,13 +123,15 @@ export async function delegateDelayedLeave({
|
|||||||
slotId,
|
slotId,
|
||||||
delayId,
|
delayId,
|
||||||
}: DelegateDelayedLeaveParams): Promise<void> {
|
}: DelegateDelayedLeaveParams): Promise<void> {
|
||||||
await doNetworkOperationWithRetry(async () =>
|
await doNetworkOperationWithRetry(
|
||||||
client._unstable_delegateDelayedLeave({
|
async () =>
|
||||||
url,
|
client._unstable_delegateDelayedLeave({
|
||||||
room_id: roomId,
|
url,
|
||||||
slot_id: slotId,
|
room_id: roomId,
|
||||||
member_id: membership.memberId,
|
slot_id: slotId,
|
||||||
delay_id: delayId,
|
member_id: membership.memberId,
|
||||||
}),
|
delay_id: delayId,
|
||||||
|
}),
|
||||||
|
isNetworkError,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -158,6 +158,34 @@ describe("getSFUConfig", () => {
|
|||||||
expect(openID.getSFUConfig).toHaveBeenCalled();
|
expect(openID.getSFUConfig).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("falls back to OpenID when the widget host refuses the CS API", async () => {
|
||||||
|
// In widget mode a host without MSC4533 rejects the request before it
|
||||||
|
// reaches the homeserver, with a plain error rather than a Matrix error
|
||||||
|
client._unstable_getLivekitToken.mockRejectedValueOnce(
|
||||||
|
new Error(
|
||||||
|
"The rtc_livekit_get_token action is not supported by the client.",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
await getSFUConfig({
|
||||||
|
...defaultParams,
|
||||||
|
transport: urlAndServiceUrlTransport,
|
||||||
|
}),
|
||||||
|
).toEqual(expect.objectContaining({ jwt: openIDJWTToken }));
|
||||||
|
expect(openID.getSFUConfig).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("surfaces the widget host's refusal with url transport", async () => {
|
||||||
|
const error = new Error(
|
||||||
|
"The rtc_livekit_get_token action is not supported by the client.",
|
||||||
|
);
|
||||||
|
client._unstable_getLivekitToken.mockRejectedValueOnce(error);
|
||||||
|
await expect(
|
||||||
|
getSFUConfig({ ...defaultParams, transport: urlTransport }),
|
||||||
|
).rejects.toThrow(error);
|
||||||
|
expect(openID.getSFUConfig).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it("does not fall back to OpenID when publishing", async () => {
|
it("does not fall back to OpenID when publishing", async () => {
|
||||||
client._unstable_getLivekitToken.mockRejectedValueOnce(unsupportedError);
|
client._unstable_getLivekitToken.mockRejectedValueOnce(unsupportedError);
|
||||||
await expect(
|
await expect(
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ Please see LICENSE in the repository root for full details.
|
|||||||
import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager";
|
import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager";
|
||||||
import { type Logger } from "matrix-js-sdk/lib/logger";
|
import { type Logger } from "matrix-js-sdk/lib/logger";
|
||||||
|
|
||||||
import { MatrixError } from "matrix-js-sdk";
|
import { ConnectionError, MatrixError } from "matrix-js-sdk";
|
||||||
import { type UnstableLivekitTransport } from "matrix-js-sdk/lib/matrixrtc";
|
import { type UnstableLivekitTransport } from "matrix-js-sdk/lib/matrixrtc";
|
||||||
import * as openID from "./openID";
|
import * as openID from "./openID";
|
||||||
import * as csApi from "./csApi";
|
import * as csApi from "./csApi";
|
||||||
@@ -38,6 +38,19 @@ function isEndpointUnsupported(e: unknown): boolean {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether an error means the MSC4195 CS API cannot be used from here:
|
||||||
|
* the homeserver does not implement it, or the request never reached the
|
||||||
|
* homeserver because in widget mode the host does not expose the endpoint
|
||||||
|
* (MSC4533), which the widget API reports as a plain error rather than a
|
||||||
|
* Matrix error. A connection error is neither, as the endpoint may well exist.
|
||||||
|
* @param e The error to check.
|
||||||
|
*/
|
||||||
|
function isCsApiUnavailable(e: unknown): boolean {
|
||||||
|
if (isEndpointUnsupported(e)) return true;
|
||||||
|
return !(e instanceof MatrixError) && !(e instanceof ConnectionError);
|
||||||
|
}
|
||||||
|
|
||||||
export interface GetSFUConfigParams {
|
export interface GetSFUConfigParams {
|
||||||
/**
|
/**
|
||||||
* The Matrix client.
|
* The Matrix client.
|
||||||
@@ -113,23 +126,25 @@ export async function getSFUConfig({
|
|||||||
slotId,
|
slotId,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!isEndpointUnsupported(e)) throw e;
|
|
||||||
// Publishers should never fall back. If a homeserver advertises a `url`
|
// Publishers should never fall back. If a homeserver advertises a `url`
|
||||||
// but doesn't support MSC4195 itself, that's a misconfiguration which
|
// but doesn't support MSC4195 itself, that's a misconfiguration which
|
||||||
// the admin should know about so they can prepare for the future
|
// the admin should know about so they can prepare for the future
|
||||||
// deprecation of `livekit_service_url`.
|
// deprecation of `livekit_service_url`.
|
||||||
const mayFallBack =
|
const mayFallBack =
|
||||||
role === "subscriber" && "livekit_service_url" in transport;
|
role === "subscriber" && "livekit_service_url" in transport;
|
||||||
if (!mayFallBack) {
|
if (mayFallBack && isCsApiUnavailable(e)) {
|
||||||
|
logger.warn(
|
||||||
|
`Cannot use the MSC4195 get_token endpoint (${e}), falling back to the OpenID flow with service ${transport.livekit_service_url}`,
|
||||||
|
);
|
||||||
|
return await viaOpenID(transport.livekit_service_url);
|
||||||
|
}
|
||||||
|
if (isEndpointUnsupported(e)) {
|
||||||
logger.error(
|
logger.error(
|
||||||
`Homeserver does not support the MSC4195 get_token endpoint, cannot get token for transport ${flow.url}`,
|
`Homeserver does not support the MSC4195 get_token endpoint, cannot get token for transport ${flow.url}`,
|
||||||
);
|
);
|
||||||
throw new NoMatrix2AuthorizationService(e as Error);
|
throw new NoMatrix2AuthorizationService(e as Error);
|
||||||
}
|
}
|
||||||
logger.warn(
|
throw e;
|
||||||
`Homeserver does not support the MSC4195 get_token endpoint, falling back to the OpenID flow with service ${transport.livekit_service_url}`,
|
|
||||||
);
|
|
||||||
return await viaOpenID(transport.livekit_service_url);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+7
-1
@@ -341,9 +341,13 @@ export function getRelativeRoomUrl(
|
|||||||
* Perform a network operation with retries on ConnectionError.
|
* Perform a network operation with retries on ConnectionError.
|
||||||
* If the error is not retryable, or the max number of retries is reached, the error is rethrown.
|
* If the error is not retryable, or the max number of retries is reached, the error is rethrown.
|
||||||
* Supports handling of matrix quotas.
|
* Supports handling of matrix quotas.
|
||||||
|
* @param operation The operation to perform.
|
||||||
|
* @param shouldRetry Rules out errors that retrying cannot fix, on top of the
|
||||||
|
* standard backoff rule. Retries everything by default.
|
||||||
*/
|
*/
|
||||||
export async function doNetworkOperationWithRetry<T>(
|
export async function doNetworkOperationWithRetry<T>(
|
||||||
operation: () => Promise<T>,
|
operation: () => Promise<T>,
|
||||||
|
shouldRetry: (e: unknown) => boolean = () => true,
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
let currentRetryCount = 0;
|
let currentRetryCount = 0;
|
||||||
|
|
||||||
@@ -353,7 +357,9 @@ export async function doNetworkOperationWithRetry<T>(
|
|||||||
return await operation();
|
return await operation();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
currentRetryCount++;
|
currentRetryCount++;
|
||||||
const backoff = calculateRetryBackoff(e, currentRetryCount, true);
|
const backoff = shouldRetry(e)
|
||||||
|
? calculateRetryBackoff(e, currentRetryCount, true)
|
||||||
|
: -1;
|
||||||
if (backoff < 0) {
|
if (backoff < 0) {
|
||||||
// Max number of retries reached, or error is not retryable. rethrow the error
|
// Max number of retries reached, or error is not retryable. rethrow the error
|
||||||
throw e;
|
throw e;
|
||||||
|
|||||||
Reference in New Issue
Block a user