diff --git a/package.json b/package.json index 815fe8cf44..8f6fc95d8a 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/spec/unit/embedded.spec.ts b/spec/unit/embedded.spec.ts index dc9952465a..b09d3a2709 100644 --- a/spec/unit/embedded.spec.ts +++ b/spec/unit/embedded.spec.ts @@ -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 () => { diff --git a/spec/unit/http-api/errors.spec.ts b/spec/unit/http-api/errors.spec.ts index 6054aad4bb..bcf3aa4554 100644 --- a/spec/unit/http-api/errors.spec.ts +++ b/spec/unit/http-api/errors.spec.ts @@ -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); + }); + }); }); diff --git a/src/embedded.ts b/src/embedded.ts index a2be6209cd..be03037f97 100644 --- a/src/embedded.ts +++ b/src/embedded.ts @@ -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 => { + try { + return await transportSend(action, data); + } catch (error) { + processAndThrow(error); + } + }; + + const transportSendComplete = this.widgetApi.transport.sendComplete.bind(this.widgetApi.transport); + this.widgetApi.transport.sendComplete = async ( + action: WidgetApiAction, + data: T, + ): Promise => { + try { + return await transportSendComplete(action, data); + } catch (error) { + processAndThrow(error); + } + }; + this.widgetApiReady = new Promise((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; + } +} diff --git a/src/http-api/errors.ts b/src/http-api/errors.ts index f80c3fdd8b..ab02ccfcee 100644 --- a/src/http-api/errors.ts +++ b/src/http-api/errors.ts @@ -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 = {}; + 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)); + } } /** diff --git a/yarn.lock b/yarn.lock index 147a753f15..e999f54328 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"