Skip to content

Commit 6abf9f9

Browse files
committed
(De)serialize error response details
Allow client widget drivers to serialize Matrix API error responses into JSON to be received by the requesting widget.
1 parent 994a8b2 commit 6abf9f9

File tree

7 files changed

+442
-98
lines changed

7 files changed

+442
-98
lines changed

src/ClientWidgetApi.ts

Lines changed: 22 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import { WidgetApiDirection } from "./interfaces/WidgetApiDirection";
2222
import { IWidgetApiRequest, IWidgetApiRequestEmptyData } from "./interfaces/IWidgetApiRequest";
2323
import { IContentLoadedActionRequest } from "./interfaces/ContentLoadedAction";
2424
import { WidgetApiFromWidgetAction, WidgetApiToWidgetAction } from "./interfaces/WidgetApiAction";
25-
import { IWidgetApiErrorResponseData, isMatrixError } from "./interfaces/IWidgetApiErrorResponse";
25+
import { IWidgetApiErrorResponseData } from "./interfaces/IWidgetApiErrorResponse";
2626
import { Capability, MatrixCapabilities } from "./interfaces/Capabilities";
2727
import { IOpenIDUpdate, ISendEventDetails, ISendDelayedEventDetails, WidgetDriver } from "./driver/WidgetDriver";
2828
import {
@@ -330,15 +330,13 @@ export class ClientWidgetApi extends EventEmitter {
330330
});
331331
}
332332

333-
const onErr = (e: any) => {
333+
const onErr = (e: unknown) => {
334334
console.error("[ClientWidgetApi] Failed to handle navigation: ", e);
335-
return this.transport.reply<IWidgetApiErrorResponseData>(request, {
336-
error: {message: "Error handling navigation"},
337-
});
335+
this.handleDriverError(e, request, "Error handling navigation");
338336
};
339337

340338
try {
341-
this.driver.navigate(request.data.uri.toString()).catch(e => onErr(e)).then(() => {
339+
this.driver.navigate(request.data.uri.toString()).catch((e: unknown) => onErr(e)).then(() => {
342340
return this.transport.reply<IWidgetApiAcknowledgeResponseData>(request, {});
343341
});
344342
} catch (e) {
@@ -556,12 +554,7 @@ export class ClientWidgetApi extends EventEmitter {
556554
});
557555
}).catch((e: unknown) => {
558556
console.error("error sending event: ", e);
559-
return this.transport.reply<IWidgetApiErrorResponseData>(request, {
560-
error: {
561-
message: "Error sending event",
562-
...(isMatrixError(e) && e),
563-
},
564-
});
557+
this.handleDriverError(e, request, "Error sending event");
565558
});
566559
}
567560

@@ -586,12 +579,7 @@ export class ClientWidgetApi extends EventEmitter {
586579
return this.transport.reply<IWidgetApiAcknowledgeResponseData>(request, {});
587580
}).catch((e: unknown) => {
588581
console.error("error updating delayed event: ", e);
589-
return this.transport.reply<IWidgetApiErrorResponseData>(request, {
590-
error: {
591-
message: "Error updating delayed event",
592-
...(isMatrixError(e) && e),
593-
},
594-
});
582+
this.handleDriverError(e, request, "Error updating delayed event");
595583
});
596584
break;
597585
default:
@@ -624,9 +612,7 @@ export class ClientWidgetApi extends EventEmitter {
624612
await this.transport.reply<ISendToDeviceFromWidgetResponseData>(request, {});
625613
} catch (e) {
626614
console.error("error sending to-device event", e);
627-
await this.transport.reply<IWidgetApiErrorResponseData>(request, {
628-
error: {message: "Error sending event"},
629-
});
615+
this.handleDriverError(e, request, "Error sending event");
630616
}
631617
}
632618
}
@@ -741,12 +727,7 @@ export class ClientWidgetApi extends EventEmitter {
741727
);
742728
} catch (e) {
743729
console.error("error getting the relations", e);
744-
await this.transport.reply<IWidgetApiErrorResponseData>(request, {
745-
error: {
746-
message: "Unexpected error while reading relations",
747-
...(isMatrixError(e) && e),
748-
},
749-
});
730+
this.handleDriverError(e, request, "Unexpected error while reading relations");
750731
}
751732
}
752733

@@ -787,12 +768,7 @@ export class ClientWidgetApi extends EventEmitter {
787768
);
788769
} catch (e) {
789770
console.error("error searching in the user directory", e);
790-
await this.transport.reply<IWidgetApiErrorResponseData>(request, {
791-
error: {
792-
message: "Unexpected error while searching in the user directory",
793-
...(isMatrixError(e) && e),
794-
},
795-
});
771+
this.handleDriverError(e, request, "Unexpected error while searching in the user directory");
796772
}
797773
}
798774

@@ -812,12 +788,7 @@ export class ClientWidgetApi extends EventEmitter {
812788
);
813789
} catch (e) {
814790
console.error("error while getting the media configuration", e);
815-
await this.transport.reply<IWidgetApiErrorResponseData>(request, {
816-
error: {
817-
message: "Unexpected error while getting the media configuration",
818-
...(isMatrixError(e) && e),
819-
},
820-
});
791+
this.handleDriverError(e, request, "Unexpected error while getting the media configuration");
821792
}
822793
}
823794

@@ -837,12 +808,7 @@ export class ClientWidgetApi extends EventEmitter {
837808
);
838809
} catch (e) {
839810
console.error("error while uploading a file", e);
840-
await this.transport.reply<IWidgetApiErrorResponseData>(request, {
841-
error: {
842-
message: "Unexpected error while uploading a file",
843-
...(isMatrixError(e) && e),
844-
},
845-
});
811+
this.handleDriverError(e, request, "Unexpected error while uploading a file");
846812
}
847813
}
848814

@@ -862,12 +828,20 @@ export class ClientWidgetApi extends EventEmitter {
862828
);
863829
} catch (e) {
864830
console.error("error while downloading a file", e);
865-
this.transport.reply<IWidgetApiErrorResponseData>(request, {
866-
error: { message: "Unexpected error while downloading a file" },
867-
});
831+
this.handleDriverError(e, request, "Unexpected error while downloading a file");
868832
}
869833
}
870834

835+
private handleDriverError(e: unknown, request: IWidgetApiRequest, message: string) {
836+
const matrixApiError = this.driver.processError(e);
837+
this.transport.reply<IWidgetApiErrorResponseData>(request, {
838+
error: {
839+
message,
840+
...(matrixApiError && { matrix_api_error: { ...matrixApiError } }),
841+
},
842+
});
843+
}
844+
871845
private handleMessage(ev: CustomEvent<IWidgetApiRequest>) {
872846
if (this.isStopped) return;
873847
const actionEv = new CustomEvent(`action:${ev.detail.action}`, {

src/WidgetApi.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import {
3333
import { ITransport } from "./transport/ITransport";
3434
import { PostmessageTransport } from "./transport/PostmessageTransport";
3535
import { WidgetApiFromWidgetAction, WidgetApiToWidgetAction } from "./interfaces/WidgetApiAction";
36-
import { IWidgetApiErrorResponseData } from "./interfaces/IWidgetApiErrorResponse";
36+
import { IWidgetApiErrorResponseData, IWidgetApiErrorResponseDataDetails } from "./interfaces/IWidgetApiErrorResponse";
3737
import { IStickerActionRequestData } from "./interfaces/StickerAction";
3838
import { IStickyActionRequestData, IStickyActionResponseData } from "./interfaces/StickyAction";
3939
import {
@@ -95,6 +95,15 @@ import {
9595
UpdateDelayedEventAction,
9696
} from "./interfaces/UpdateDelayedEventAction";
9797

98+
export class WidgetApiResponseError extends Error {
99+
public constructor(
100+
message: string,
101+
public readonly data: IWidgetApiErrorResponseDataDetails,
102+
) {
103+
super(message);
104+
}
105+
}
106+
98107
/**
99108
* API handler for widgets. This raises events for each action
100109
* received as `action:${action}` (eg: "action:screenshot").

src/driver/WidgetDriver.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
IOpenIDCredentials,
2020
OpenIDRequestState,
2121
SimpleObservable,
22+
IMatrixApiError,
2223
IRoomEvent,
2324
IRoomAccountData,
2425
ITurnServer,
@@ -358,4 +359,15 @@ export abstract class WidgetDriver {
358359
): Promise<{ file: XMLHttpRequestBodyInit }> {
359360
throw new Error("Download file is not implemented");
360361
}
362+
363+
/**
364+
* Expresses an error thrown by a Matrix API request made by this driver
365+
* in a format compatible with the Widget API.
366+
* @param error The error to handle.
367+
* @returns The error expressed as a {@link IMatrixApiError},
368+
* or undefined if it cannot be expressed as one.
369+
*/
370+
public processError(error: unknown): IMatrixApiError | undefined {
371+
return undefined;
372+
}
361373
}
Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020 The Matrix.org Foundation C.I.C.
2+
* Copyright 2020 - 2024 The Matrix.org Foundation C.I.C.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,34 +16,42 @@
1616

1717
import { IWidgetApiResponse, IWidgetApiResponseData } from "./IWidgetApiResponse";
1818

19-
interface IWidgetApiErrorData {
20-
message: string;
19+
/**
20+
* The format of errors returned by Matrix API requests
21+
* made by a WidgetDriver.
22+
*/
23+
export interface IMatrixApiError {
24+
/** The HTTP status code of the associated request. */
25+
http_status: number; // eslint-disable-line camelcase
26+
/** Any HTTP response headers that are relevant to the error. */
27+
http_headers: {[name: string]: string}; // eslint-disable-line camelcase
28+
/** The URL of the failed request. */
29+
url: string;
30+
/** @see {@link https://spec.matrix.org/latest/client-server-api/#standard-error-response} */
31+
response: {
32+
errcode: string;
33+
error: string;
34+
} & IWidgetApiResponseData; // extensible
2135
}
2236

23-
interface IMatrixErrorData {
24-
httpStatus?: number;
25-
errcode?: string;
37+
export interface IWidgetApiErrorResponseDataDetails extends IWidgetApiResponseData {
38+
/** Set if the error came from a Matrix API request made by a widget driver */
39+
matrix_api_error?: IMatrixApiError; // eslint-disable-line camelcase
2640
}
2741

2842
export interface IWidgetApiErrorResponseData extends IWidgetApiResponseData {
29-
error: IWidgetApiErrorData & IMatrixErrorData;
30-
}
31-
32-
export function isMatrixError(err: unknown): err is IMatrixErrorData {
33-
return typeof err === "object" && err !== null && (
34-
"httpStatus" in err && typeof err.httpStatus === "number" ||
35-
"errcode" in err && typeof err.errcode === "string"
36-
);
43+
error: {
44+
/** A user-friendly string describing the error */
45+
message: string;
46+
} & IWidgetApiErrorResponseDataDetails;
3747
}
3848

3949
export interface IWidgetApiErrorResponse extends IWidgetApiResponse {
4050
response: IWidgetApiErrorResponseData;
4151
}
4252

43-
export function isErrorResponse(responseData: IWidgetApiResponseData): boolean {
44-
if ("error" in responseData) {
45-
const err = <IWidgetApiErrorResponseData>responseData;
46-
return !!err.error.message;
47-
}
48-
return false;
53+
export function isErrorResponse(responseData: IWidgetApiResponseData): responseData is IWidgetApiErrorResponseData {
54+
const error = responseData.error;
55+
return typeof error === "object" && error !== null &&
56+
"message" in error && typeof error.message === "string";
4957
}

src/transport/ITransport.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020 The Matrix.org Foundation C.I.C.
2+
* Copyright 2020 - 2024 The Matrix.org Foundation C.I.C.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -71,11 +71,12 @@ export interface ITransport extends EventEmitter {
7171

7272
/**
7373
* Sends a request to the remote end.
74-
* @param {WidgetApiAction} action The action to send.
75-
* @param {IWidgetApiRequestData} data The request data.
76-
* @returns {Promise<IWidgetApiResponseData>} A promise which resolves
77-
* to the remote end's response, or throws with an Error if the request
78-
* failed.
74+
* @param action The action to send.
75+
* @param data The request data.
76+
* @returns A promise which resolves to the remote end's response.
77+
* @throws {Error} if the request failed with a generic error.
78+
* @throws {WidgetApiResponseError} if the request failed with error details
79+
* that can be communicated to the Widget API.
7980
*/
8081
send<T extends IWidgetApiRequestData, R extends IWidgetApiResponseData = IWidgetApiAcknowledgeResponseData>(
8182
action: WidgetApiAction,
@@ -88,9 +89,10 @@ export interface ITransport extends EventEmitter {
8889
* data.
8990
* @param {WidgetApiAction} action The action to send.
9091
* @param {IWidgetApiRequestData} data The request data.
91-
* @returns {Promise<IWidgetApiResponseData>} A promise which resolves
92-
* to the remote end's response, or throws with an Error if the request
93-
* failed.
92+
* @returns {Promise<IWidgetApiResponseData>} A promise which resolves to the remote end's response
93+
* @throws {Error} if the request failed with a generic error.
94+
* @throws {WidgetApiResponseError} if the request failed with error details
95+
* that can be communicated to the Widget API.
9496
*/
9597
sendComplete<T extends IWidgetApiRequestData, R extends IWidgetApiResponse>(action: WidgetApiAction, data: T)
9698
: Promise<R>;

src/transport/PostmessageTransport.ts

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020 The Matrix.org Foundation C.I.C.
2+
* Copyright 2020 - 2024 The Matrix.org Foundation C.I.C.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -19,11 +19,11 @@ import { ITransport } from "./ITransport";
1919
import {
2020
invertedDirection,
2121
isErrorResponse,
22-
IWidgetApiErrorResponseData,
2322
IWidgetApiRequest,
2423
IWidgetApiRequestData,
2524
IWidgetApiResponse,
2625
IWidgetApiResponseData,
26+
WidgetApiResponseError,
2727
WidgetApiAction,
2828
WidgetApiDirection,
2929
WidgetApiToWidgetAction,
@@ -35,16 +35,6 @@ interface IOutboundRequest {
3535
reject: (err: Error) => void;
3636
}
3737

38-
class MatrixError extends Error {
39-
public constructor(
40-
msg: string,
41-
public readonly httpStatus?: number,
42-
public readonly errcode?: string,
43-
) {
44-
super(msg);
45-
}
46-
}
47-
4838
/**
4939
* Transport for the Widget API over postMessage.
5040
*/
@@ -204,12 +194,8 @@ export class PostmessageTransport extends EventEmitter implements ITransport {
204194
if (!req) return; // response to an unknown request
205195

206196
if (isErrorResponse(response.response)) {
207-
const err = <IWidgetApiErrorResponseData>response.response;
208-
req.reject(
209-
err.error.httpStatus !== undefined || err.error.errcode !== undefined
210-
? new MatrixError(err.error.message, err.error.httpStatus, err.error.errcode)
211-
: new Error(err.error.message),
212-
);
197+
const {message, ...data} = response.response.error;
198+
req.reject(new WidgetApiResponseError(message, data));
213199
} else {
214200
req.resolve(response);
215201
}

0 commit comments

Comments
 (0)