Skip to content

Commit 98f7637

Browse files
authored
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
1 parent 0df8e81 commit 98f7637

File tree

6 files changed

+255
-9
lines changed

6 files changed

+255
-9
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@
5858
"jwt-decode": "^4.0.0",
5959
"loglevel": "^1.7.1",
6060
"matrix-events-sdk": "0.0.1",
61-
"matrix-widget-api": "^1.8.2",
61+
"matrix-widget-api": "^1.10.0",
6262
"oidc-client-ts": "^3.0.1",
6363
"p-retry": "4",
6464
"sdp-transform": "^2.14.1",

spec/unit/embedded.spec.ts

Lines changed: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,10 @@ import {
3030
ITurnServer,
3131
IRoomEvent,
3232
IOpenIDCredentials,
33+
WidgetApiResponseError,
3334
} from "matrix-widget-api";
3435

35-
import { createRoomWidgetClient, MsgType, UpdateDelayedEventAction } from "../../src/matrix";
36+
import { createRoomWidgetClient, MatrixError, MsgType, UpdateDelayedEventAction } from "../../src/matrix";
3637
import { MatrixClient, ClientEvent, ITurnServer as IClientTurnServer } from "../../src/client";
3738
import { SyncState } from "../../src/sync";
3839
import { ICapabilities, RoomWidgetClient } from "../../src/embedded";
@@ -90,7 +91,11 @@ class MockWidgetApi extends EventEmitter {
9091
public getTurnServers = jest.fn(() => []);
9192
public sendContentLoaded = jest.fn();
9293

93-
public transport = { reply: jest.fn() };
94+
public transport = {
95+
reply: jest.fn(),
96+
send: jest.fn(),
97+
sendComplete: jest.fn(),
98+
};
9499
}
95100

96101
declare module "../../src/types" {
@@ -187,6 +192,46 @@ describe("RoomWidgetClient", () => {
187192
.map((e) => e.getEffectiveEvent()),
188193
).toEqual([event]);
189194
});
195+
196+
it("handles widget errors with generic error data", async () => {
197+
const error = new Error("failed to send");
198+
widgetApi.transport.send.mockRejectedValue(error);
199+
200+
await makeClient({ sendEvent: ["org.matrix.rageshake_request"] });
201+
widgetApi.sendRoomEvent.mockImplementation(widgetApi.transport.send);
202+
203+
await expect(
204+
client.sendEvent("!1:example.org", "org.matrix.rageshake_request", { request_id: 123 }),
205+
).rejects.toThrow(error);
206+
});
207+
208+
it("handles widget errors with Matrix API error response data", async () => {
209+
const errorStatusCode = 400;
210+
const errorUrl = "http://example.org";
211+
const errorData = {
212+
errcode: "M_BAD_JSON",
213+
error: "Invalid body",
214+
};
215+
216+
const widgetError = new WidgetApiResponseError("failed to send", {
217+
matrix_api_error: {
218+
http_status: errorStatusCode,
219+
http_headers: {},
220+
url: errorUrl,
221+
response: errorData,
222+
},
223+
});
224+
const matrixError = new MatrixError(errorData, errorStatusCode, errorUrl);
225+
226+
widgetApi.transport.send.mockRejectedValue(widgetError);
227+
228+
await makeClient({ sendEvent: ["org.matrix.rageshake_request"] });
229+
widgetApi.sendRoomEvent.mockImplementation(widgetApi.transport.send);
230+
231+
await expect(
232+
client.sendEvent("!1:example.org", "org.matrix.rageshake_request", { request_id: 123 }),
233+
).rejects.toThrow(matrixError);
234+
});
190235
});
191236

192237
describe("delayed events", () => {
@@ -598,6 +643,42 @@ describe("RoomWidgetClient", () => {
598643
await makeClient({});
599644
expect(await client.getOpenIdToken()).toStrictEqual(testOIDCToken);
600645
});
646+
647+
it("handles widget errors with generic error data", async () => {
648+
const error = new Error("failed to get token");
649+
widgetApi.transport.sendComplete.mockRejectedValue(error);
650+
651+
await makeClient({});
652+
widgetApi.requestOpenIDConnectToken.mockImplementation(widgetApi.transport.sendComplete as any);
653+
654+
await expect(client.getOpenIdToken()).rejects.toThrow(error);
655+
});
656+
657+
it("handles widget errors with Matrix API error response data", async () => {
658+
const errorStatusCode = 400;
659+
const errorUrl = "http://example.org";
660+
const errorData = {
661+
errcode: "M_UNKNOWN",
662+
error: "Bad request",
663+
};
664+
665+
const widgetError = new WidgetApiResponseError("failed to get token", {
666+
matrix_api_error: {
667+
http_status: errorStatusCode,
668+
http_headers: {},
669+
url: errorUrl,
670+
response: errorData,
671+
},
672+
});
673+
const matrixError = new MatrixError(errorData, errorStatusCode, errorUrl);
674+
675+
widgetApi.transport.sendComplete.mockRejectedValue(widgetError);
676+
677+
await makeClient({});
678+
widgetApi.requestOpenIDConnectToken.mockImplementation(widgetApi.transport.sendComplete as any);
679+
680+
await expect(client.getOpenIdToken()).rejects.toThrow(matrixError);
681+
});
601682
});
602683

603684
it("gets TURN servers", async () => {

spec/unit/http-api/errors.spec.ts

Lines changed: 93 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ describe("MatrixError", () => {
2525
headers = new Headers({ "Content-Type": "application/json" });
2626
});
2727

28-
function makeMatrixError(httpStatus: number, data: IErrorJson): MatrixError {
29-
return new MatrixError(data, httpStatus, undefined, undefined, headers);
28+
function makeMatrixError(httpStatus: number, data: IErrorJson, url?: string): MatrixError {
29+
return new MatrixError(data, httpStatus, url, undefined, headers);
3030
}
3131

3232
it("should accept absent retry time from rate-limit error", () => {
@@ -95,4 +95,95 @@ describe("MatrixError", () => {
9595
const err = makeMatrixError(429, { errcode: "M_LIMIT_EXCEEDED" });
9696
expect(() => err.getRetryAfterMs()).toThrow("integer value is too large");
9797
});
98+
99+
describe("can be converted to data compatible with the widget api", () => {
100+
it("from default values", () => {
101+
const matrixError = new MatrixError();
102+
103+
const widgetApiErrorData = {
104+
http_status: 400,
105+
http_headers: {},
106+
url: "",
107+
response: {
108+
errcode: "M_UNKNOWN",
109+
error: "Unknown message",
110+
},
111+
};
112+
113+
expect(matrixError.asWidgetApiErrorData()).toEqual(widgetApiErrorData);
114+
});
115+
116+
it("from non-default values", () => {
117+
headers.set("Retry-After", "120");
118+
const statusCode = 429;
119+
const data = {
120+
errcode: "M_LIMIT_EXCEEDED",
121+
error: "Request is rate-limited.",
122+
retry_after_ms: 120000,
123+
};
124+
const url = "http://example.net";
125+
126+
const matrixError = makeMatrixError(statusCode, data, url);
127+
128+
const widgetApiErrorData = {
129+
http_status: statusCode,
130+
http_headers: {
131+
"content-type": "application/json",
132+
"retry-after": "120",
133+
},
134+
url,
135+
response: data,
136+
};
137+
138+
expect(matrixError.asWidgetApiErrorData()).toEqual(widgetApiErrorData);
139+
});
140+
});
141+
142+
describe("can be created from data received from the widget api", () => {
143+
it("from minimal data", () => {
144+
const statusCode = 400;
145+
const data = {
146+
errcode: "M_UNKNOWN",
147+
error: "Something went wrong.",
148+
};
149+
const url = "";
150+
151+
const widgetApiErrorData = {
152+
http_status: statusCode,
153+
http_headers: {},
154+
url,
155+
response: data,
156+
};
157+
158+
headers.delete("Content-Type");
159+
const matrixError = makeMatrixError(statusCode, data, url);
160+
161+
expect(MatrixError.fromWidgetApiErrorData(widgetApiErrorData)).toEqual(matrixError);
162+
});
163+
164+
it("from more data", () => {
165+
const statusCode = 429;
166+
const data = {
167+
errcode: "M_LIMIT_EXCEEDED",
168+
error: "Request is rate-limited.",
169+
retry_after_ms: 120000,
170+
};
171+
const url = "http://example.net";
172+
173+
const widgetApiErrorData = {
174+
http_status: statusCode,
175+
http_headers: {
176+
"content-type": "application/json",
177+
"retry-after": "120",
178+
},
179+
url,
180+
response: data,
181+
};
182+
183+
headers.set("Retry-After", "120");
184+
const matrixError = makeMatrixError(statusCode, data, url);
185+
186+
expect(MatrixError.fromWidgetApiErrorData(widgetApiErrorData)).toEqual(matrixError);
187+
});
188+
});
98189
});

src/embedded.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,17 @@ limitations under the License.
1717
import {
1818
WidgetApi,
1919
WidgetApiToWidgetAction,
20+
WidgetApiResponseError,
2021
MatrixCapabilities,
2122
IWidgetApiRequest,
2223
IWidgetApiAcknowledgeResponseData,
2324
ISendEventToWidgetActionRequest,
2425
ISendToDeviceToWidgetActionRequest,
2526
ISendEventFromWidgetResponseData,
27+
IWidgetApiRequestData,
28+
WidgetApiAction,
29+
IWidgetApiResponse,
30+
IWidgetApiResponseData,
2631
} from "matrix-widget-api";
2732

2833
import { MatrixEvent, IEvent, IContent, EventStatus } from "./models/event.ts";
@@ -45,6 +50,7 @@ import {
4550
} from "./client.ts";
4651
import { SyncApi, SyncState } from "./sync.ts";
4752
import { SlidingSyncSdk } from "./sliding-sync-sdk.ts";
53+
import { MatrixError } from "./http-api/errors.ts";
4854
import { User } from "./models/user.ts";
4955
import { Room } from "./models/room.ts";
5056
import { ToDeviceBatch, ToDevicePayload } from "./models/ToDeviceMessage.ts";
@@ -147,6 +153,33 @@ export class RoomWidgetClient extends MatrixClient {
147153
) {
148154
super(opts);
149155

156+
const transportSend = this.widgetApi.transport.send.bind(this.widgetApi.transport);
157+
this.widgetApi.transport.send = async <
158+
T extends IWidgetApiRequestData,
159+
R extends IWidgetApiResponseData = IWidgetApiAcknowledgeResponseData,
160+
>(
161+
action: WidgetApiAction,
162+
data: T,
163+
): Promise<R> => {
164+
try {
165+
return await transportSend<T, R>(action, data);
166+
} catch (error) {
167+
processAndThrow(error);
168+
}
169+
};
170+
171+
const transportSendComplete = this.widgetApi.transport.sendComplete.bind(this.widgetApi.transport);
172+
this.widgetApi.transport.sendComplete = async <T extends IWidgetApiRequestData, R extends IWidgetApiResponse>(
173+
action: WidgetApiAction,
174+
data: T,
175+
): Promise<R> => {
176+
try {
177+
return await transportSendComplete<T, R>(action, data);
178+
} catch (error) {
179+
processAndThrow(error);
180+
}
181+
};
182+
150183
this.widgetApiReady = new Promise<void>((resolve) => this.widgetApi.once("ready", resolve));
151184

152185
// Request capabilities for the functionality this client needs to support
@@ -523,3 +556,11 @@ export class RoomWidgetClient extends MatrixClient {
523556
}
524557
}
525558
}
559+
560+
function processAndThrow(error: unknown): never {
561+
if (error instanceof WidgetApiResponseError && error.data.matrix_api_error) {
562+
throw MatrixError.fromWidgetApiErrorData(error.data.matrix_api_error);
563+
} else {
564+
throw error;
565+
}
566+
}

src/http-api/errors.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17+
import { IMatrixApiError as IWidgetMatrixError } from "matrix-widget-api";
18+
1719
import { IUsageLimit } from "../@types/partials.ts";
1820
import { MatrixEvent } from "../models/event.ts";
1921

@@ -131,6 +133,37 @@ export class MatrixError extends HTTPError {
131133
}
132134
return null;
133135
}
136+
137+
/**
138+
* @returns this error expressed as a JSON payload
139+
* for use by Widget API error responses.
140+
*/
141+
public asWidgetApiErrorData(): IWidgetMatrixError {
142+
const headers: Record<string, string> = {};
143+
if (this.httpHeaders) {
144+
for (const [name, value] of this.httpHeaders) {
145+
headers[name] = value;
146+
}
147+
}
148+
return {
149+
http_status: this.httpStatus ?? 400,
150+
http_headers: headers,
151+
url: this.url ?? "",
152+
response: {
153+
errcode: this.errcode ?? "M_UNKNOWN",
154+
error: this.data.error ?? "Unknown message",
155+
...this.data,
156+
},
157+
};
158+
}
159+
160+
/**
161+
* @returns a new {@link MatrixError} from a JSON payload
162+
* received from Widget API error responses.
163+
*/
164+
public static fromWidgetApiErrorData(data: IWidgetMatrixError): MatrixError {
165+
return new MatrixError(data.response, data.http_status, data.url, undefined, new Headers(data.http_headers));
166+
}
134167
}
135168

136169
/**

yarn.lock

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4940,10 +4940,10 @@ matrix-mock-request@^2.5.0:
49404940
dependencies:
49414941
expect "^28.1.0"
49424942

4943-
matrix-widget-api@^1.8.2:
4944-
version "1.9.0"
4945-
resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.9.0.tgz#884136b405bd3c56e4ea285095c9e01ec52b6b1f"
4946-
integrity sha512-au8mqralNDqrEvaVAkU37bXOb8I9SCe+ACdPk11QWw58FKstVq31q2wRz+qWA6J+42KJ6s1DggWbG/S3fEs3jw==
4943+
matrix-widget-api@^1.10.0:
4944+
version "1.10.0"
4945+
resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.10.0.tgz#d31ea073a5871a1fb1a511ef900b0c125a37bf55"
4946+
integrity sha512-rkAJ29briYV7TJnfBVLVSKtpeBrBju15JZFSDP6wj8YdbCu1bdmlplJayQ+vYaw1x4fzI49Q+Nz3E85s46sRDw==
49474947
dependencies:
49484948
"@types/events" "^3.0.0"
49494949
events "^3.2.0"

0 commit comments

Comments
 (0)