1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-07-31 15:24:23 +03:00

Send/receive error details with widgets (#4492)

* Send/receive error details with widgets

* Fix embedded client tests

* Use all properties of error responses

* Lint

* Rewrite ternary expression as if statement

* Put typehints on overridden functions

* Lint

* Update matrix-widget-api

* Don't @link across packages

as gendoc fails when doing so.

* Add a missing docstring

* Set widget response error string to correct value

* Test conversion to/from widget error payloads

* Test processing errors thrown by widget transport

* Lint

* Test processing errors from transport.sendComplete
This commit is contained in:
Andrew Ferrazzutti
2024-11-09 02:29:04 -05:00
committed by GitHub
parent 0df8e81da4
commit 98f7637683
6 changed files with 255 additions and 9 deletions

View File

@ -58,7 +58,7 @@
"jwt-decode": "^4.0.0",
"loglevel": "^1.7.1",
"matrix-events-sdk": "0.0.1",
"matrix-widget-api": "^1.8.2",
"matrix-widget-api": "^1.10.0",
"oidc-client-ts": "^3.0.1",
"p-retry": "4",
"sdp-transform": "^2.14.1",

View File

@ -30,9 +30,10 @@ import {
ITurnServer,
IRoomEvent,
IOpenIDCredentials,
WidgetApiResponseError,
} from "matrix-widget-api";
import { createRoomWidgetClient, MsgType, UpdateDelayedEventAction } from "../../src/matrix";
import { createRoomWidgetClient, MatrixError, MsgType, UpdateDelayedEventAction } from "../../src/matrix";
import { MatrixClient, ClientEvent, ITurnServer as IClientTurnServer } from "../../src/client";
import { SyncState } from "../../src/sync";
import { ICapabilities, RoomWidgetClient } from "../../src/embedded";
@ -90,7 +91,11 @@ class MockWidgetApi extends EventEmitter {
public getTurnServers = jest.fn(() => []);
public sendContentLoaded = jest.fn();
public transport = { reply: jest.fn() };
public transport = {
reply: jest.fn(),
send: jest.fn(),
sendComplete: jest.fn(),
};
}
declare module "../../src/types" {
@ -187,6 +192,46 @@ describe("RoomWidgetClient", () => {
.map((e) => e.getEffectiveEvent()),
).toEqual([event]);
});
it("handles widget errors with generic error data", async () => {
const error = new Error("failed to send");
widgetApi.transport.send.mockRejectedValue(error);
await makeClient({ sendEvent: ["org.matrix.rageshake_request"] });
widgetApi.sendRoomEvent.mockImplementation(widgetApi.transport.send);
await expect(
client.sendEvent("!1:example.org", "org.matrix.rageshake_request", { request_id: 123 }),
).rejects.toThrow(error);
});
it("handles widget errors with Matrix API error response data", async () => {
const errorStatusCode = 400;
const errorUrl = "http://example.org";
const errorData = {
errcode: "M_BAD_JSON",
error: "Invalid body",
};
const widgetError = new WidgetApiResponseError("failed to send", {
matrix_api_error: {
http_status: errorStatusCode,
http_headers: {},
url: errorUrl,
response: errorData,
},
});
const matrixError = new MatrixError(errorData, errorStatusCode, errorUrl);
widgetApi.transport.send.mockRejectedValue(widgetError);
await makeClient({ sendEvent: ["org.matrix.rageshake_request"] });
widgetApi.sendRoomEvent.mockImplementation(widgetApi.transport.send);
await expect(
client.sendEvent("!1:example.org", "org.matrix.rageshake_request", { request_id: 123 }),
).rejects.toThrow(matrixError);
});
});
describe("delayed events", () => {
@ -598,6 +643,42 @@ describe("RoomWidgetClient", () => {
await makeClient({});
expect(await client.getOpenIdToken()).toStrictEqual(testOIDCToken);
});
it("handles widget errors with generic error data", async () => {
const error = new Error("failed to get token");
widgetApi.transport.sendComplete.mockRejectedValue(error);
await makeClient({});
widgetApi.requestOpenIDConnectToken.mockImplementation(widgetApi.transport.sendComplete as any);
await expect(client.getOpenIdToken()).rejects.toThrow(error);
});
it("handles widget errors with Matrix API error response data", async () => {
const errorStatusCode = 400;
const errorUrl = "http://example.org";
const errorData = {
errcode: "M_UNKNOWN",
error: "Bad request",
};
const widgetError = new WidgetApiResponseError("failed to get token", {
matrix_api_error: {
http_status: errorStatusCode,
http_headers: {},
url: errorUrl,
response: errorData,
},
});
const matrixError = new MatrixError(errorData, errorStatusCode, errorUrl);
widgetApi.transport.sendComplete.mockRejectedValue(widgetError);
await makeClient({});
widgetApi.requestOpenIDConnectToken.mockImplementation(widgetApi.transport.sendComplete as any);
await expect(client.getOpenIdToken()).rejects.toThrow(matrixError);
});
});
it("gets TURN servers", async () => {

View File

@ -25,8 +25,8 @@ describe("MatrixError", () => {
headers = new Headers({ "Content-Type": "application/json" });
});
function makeMatrixError(httpStatus: number, data: IErrorJson): MatrixError {
return new MatrixError(data, httpStatus, undefined, undefined, headers);
function makeMatrixError(httpStatus: number, data: IErrorJson, url?: string): MatrixError {
return new MatrixError(data, httpStatus, url, undefined, headers);
}
it("should accept absent retry time from rate-limit error", () => {
@ -95,4 +95,95 @@ describe("MatrixError", () => {
const err = makeMatrixError(429, { errcode: "M_LIMIT_EXCEEDED" });
expect(() => err.getRetryAfterMs()).toThrow("integer value is too large");
});
describe("can be converted to data compatible with the widget api", () => {
it("from default values", () => {
const matrixError = new MatrixError();
const widgetApiErrorData = {
http_status: 400,
http_headers: {},
url: "",
response: {
errcode: "M_UNKNOWN",
error: "Unknown message",
},
};
expect(matrixError.asWidgetApiErrorData()).toEqual(widgetApiErrorData);
});
it("from non-default values", () => {
headers.set("Retry-After", "120");
const statusCode = 429;
const data = {
errcode: "M_LIMIT_EXCEEDED",
error: "Request is rate-limited.",
retry_after_ms: 120000,
};
const url = "http://example.net";
const matrixError = makeMatrixError(statusCode, data, url);
const widgetApiErrorData = {
http_status: statusCode,
http_headers: {
"content-type": "application/json",
"retry-after": "120",
},
url,
response: data,
};
expect(matrixError.asWidgetApiErrorData()).toEqual(widgetApiErrorData);
});
});
describe("can be created from data received from the widget api", () => {
it("from minimal data", () => {
const statusCode = 400;
const data = {
errcode: "M_UNKNOWN",
error: "Something went wrong.",
};
const url = "";
const widgetApiErrorData = {
http_status: statusCode,
http_headers: {},
url,
response: data,
};
headers.delete("Content-Type");
const matrixError = makeMatrixError(statusCode, data, url);
expect(MatrixError.fromWidgetApiErrorData(widgetApiErrorData)).toEqual(matrixError);
});
it("from more data", () => {
const statusCode = 429;
const data = {
errcode: "M_LIMIT_EXCEEDED",
error: "Request is rate-limited.",
retry_after_ms: 120000,
};
const url = "http://example.net";
const widgetApiErrorData = {
http_status: statusCode,
http_headers: {
"content-type": "application/json",
"retry-after": "120",
},
url,
response: data,
};
headers.set("Retry-After", "120");
const matrixError = makeMatrixError(statusCode, data, url);
expect(MatrixError.fromWidgetApiErrorData(widgetApiErrorData)).toEqual(matrixError);
});
});
});

View File

@ -17,12 +17,17 @@ limitations under the License.
import {
WidgetApi,
WidgetApiToWidgetAction,
WidgetApiResponseError,
MatrixCapabilities,
IWidgetApiRequest,
IWidgetApiAcknowledgeResponseData,
ISendEventToWidgetActionRequest,
ISendToDeviceToWidgetActionRequest,
ISendEventFromWidgetResponseData,
IWidgetApiRequestData,
WidgetApiAction,
IWidgetApiResponse,
IWidgetApiResponseData,
} from "matrix-widget-api";
import { MatrixEvent, IEvent, IContent, EventStatus } from "./models/event.ts";
@ -45,6 +50,7 @@ import {
} from "./client.ts";
import { SyncApi, SyncState } from "./sync.ts";
import { SlidingSyncSdk } from "./sliding-sync-sdk.ts";
import { MatrixError } from "./http-api/errors.ts";
import { User } from "./models/user.ts";
import { Room } from "./models/room.ts";
import { ToDeviceBatch, ToDevicePayload } from "./models/ToDeviceMessage.ts";
@ -147,6 +153,33 @@ export class RoomWidgetClient extends MatrixClient {
) {
super(opts);
const transportSend = this.widgetApi.transport.send.bind(this.widgetApi.transport);
this.widgetApi.transport.send = async <
T extends IWidgetApiRequestData,
R extends IWidgetApiResponseData = IWidgetApiAcknowledgeResponseData,
>(
action: WidgetApiAction,
data: T,
): Promise<R> => {
try {
return await transportSend<T, R>(action, data);
} catch (error) {
processAndThrow(error);
}
};
const transportSendComplete = this.widgetApi.transport.sendComplete.bind(this.widgetApi.transport);
this.widgetApi.transport.sendComplete = async <T extends IWidgetApiRequestData, R extends IWidgetApiResponse>(
action: WidgetApiAction,
data: T,
): Promise<R> => {
try {
return await transportSendComplete<T, R>(action, data);
} catch (error) {
processAndThrow(error);
}
};
this.widgetApiReady = new Promise<void>((resolve) => this.widgetApi.once("ready", resolve));
// Request capabilities for the functionality this client needs to support
@ -523,3 +556,11 @@ export class RoomWidgetClient extends MatrixClient {
}
}
}
function processAndThrow(error: unknown): never {
if (error instanceof WidgetApiResponseError && error.data.matrix_api_error) {
throw MatrixError.fromWidgetApiErrorData(error.data.matrix_api_error);
} else {
throw error;
}
}

View File

@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { IMatrixApiError as IWidgetMatrixError } from "matrix-widget-api";
import { IUsageLimit } from "../@types/partials.ts";
import { MatrixEvent } from "../models/event.ts";
@ -131,6 +133,37 @@ export class MatrixError extends HTTPError {
}
return null;
}
/**
* @returns this error expressed as a JSON payload
* for use by Widget API error responses.
*/
public asWidgetApiErrorData(): IWidgetMatrixError {
const headers: Record<string, string> = {};
if (this.httpHeaders) {
for (const [name, value] of this.httpHeaders) {
headers[name] = value;
}
}
return {
http_status: this.httpStatus ?? 400,
http_headers: headers,
url: this.url ?? "",
response: {
errcode: this.errcode ?? "M_UNKNOWN",
error: this.data.error ?? "Unknown message",
...this.data,
},
};
}
/**
* @returns a new {@link MatrixError} from a JSON payload
* received from Widget API error responses.
*/
public static fromWidgetApiErrorData(data: IWidgetMatrixError): MatrixError {
return new MatrixError(data.response, data.http_status, data.url, undefined, new Headers(data.http_headers));
}
}
/**

View File

@ -4940,10 +4940,10 @@ matrix-mock-request@^2.5.0:
dependencies:
expect "^28.1.0"
matrix-widget-api@^1.8.2:
version "1.9.0"
resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.9.0.tgz#884136b405bd3c56e4ea285095c9e01ec52b6b1f"
integrity sha512-au8mqralNDqrEvaVAkU37bXOb8I9SCe+ACdPk11QWw58FKstVq31q2wRz+qWA6J+42KJ6s1DggWbG/S3fEs3jw==
matrix-widget-api@^1.10.0:
version "1.10.0"
resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.10.0.tgz#d31ea073a5871a1fb1a511ef900b0c125a37bf55"
integrity sha512-rkAJ29briYV7TJnfBVLVSKtpeBrBju15JZFSDP6wj8YdbCu1bdmlplJayQ+vYaw1x4fzI49Q+Nz3E85s46sRDw==
dependencies:
"@types/events" "^3.0.0"
events "^3.2.0"