Skip to content

Commit e62698f

Browse files
authored
Pass HTTP status code & errcode from CS-API errors (#100)
* Pass HTTP status code & errcode from CS-API errors * (De)serialize error response details Allow client widget drivers to serialize Matrix API error responses into JSON to be received by the requesting widget. * Override name property of WidgetApiResponseError * Disable babel's no-invalid-this rule because Typescript has its own version of that rule * Increase test coverage Mock client-side responses to test deserializing them on the widget side * Increase test coverage some more * Accept more than just Matrix API error details As long as the error details payload is extensible, let drivers put more data in them than just the key for Matrix API error responses. * Don't make error data payload extensible as this makes it too easy for drivers to put data in the wrong section. Still define the payload type as an interface so that it can be extended in a future version of the API. Also don't use a subfield now that non-extensibility makes the format of the details fields unambiguous. * Set some missing fields in test * Test sendToDevice in ClientWidgetApi * Test navigation in ClientWidgetApi * Add missing license year
1 parent 5d1f971 commit e62698f

File tree

9 files changed

+1622
-232
lines changed

9 files changed

+1622
-232
lines changed

.eslintrc.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ module.exports = {
3131
"files": ["src/**/*.ts", "test/**/*.ts"],
3232
"extends": ["matrix-org/ts"],
3333
"rules": {
34+
// TypeScript has its own version of this
35+
"babel/no-invalid-this": "off",
36+
3437
"quotes": "off",
3538
},
3639
}],

src/ClientWidgetApi.ts

Lines changed: 23 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -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) {
@@ -554,11 +552,9 @@ export class ClientWidgetApi extends EventEmitter {
554552
delay_id: sentEvent.delayId,
555553
}),
556554
});
557-
}).catch(e => {
555+
}).catch((e: unknown) => {
558556
console.error("error sending event: ", e);
559-
return this.transport.reply<IWidgetApiErrorResponseData>(request, {
560-
error: {message: "Error sending event"},
561-
});
557+
this.handleDriverError(e, request, "Error sending event");
562558
});
563559
}
564560

@@ -581,11 +577,9 @@ export class ClientWidgetApi extends EventEmitter {
581577
case UpdateDelayedEventAction.Send:
582578
this.driver.updateDelayedEvent(request.data.delay_id, request.data.action).then(() => {
583579
return this.transport.reply<IWidgetApiAcknowledgeResponseData>(request, {});
584-
}).catch(e => {
580+
}).catch((e: unknown) => {
585581
console.error("error updating delayed event: ", e);
586-
return this.transport.reply<IWidgetApiErrorResponseData>(request, {
587-
error: {message: "Error updating delayed event"},
588-
});
582+
this.handleDriverError(e, request, "Error updating delayed event");
589583
});
590584
break;
591585
default:
@@ -618,9 +612,7 @@ export class ClientWidgetApi extends EventEmitter {
618612
await this.transport.reply<ISendToDeviceFromWidgetResponseData>(request, {});
619613
} catch (e) {
620614
console.error("error sending to-device event", e);
621-
await this.transport.reply<IWidgetApiErrorResponseData>(request, {
622-
error: {message: "Error sending event"},
623-
});
615+
this.handleDriverError(e, request, "Error sending event");
624616
}
625617
}
626618
}
@@ -735,9 +727,7 @@ export class ClientWidgetApi extends EventEmitter {
735727
);
736728
} catch (e) {
737729
console.error("error getting the relations", e);
738-
await this.transport.reply<IWidgetApiErrorResponseData>(request, {
739-
error: { message: "Unexpected error while reading relations" },
740-
});
730+
this.handleDriverError(e, request, "Unexpected error while reading relations");
741731
}
742732
}
743733

@@ -778,9 +768,7 @@ export class ClientWidgetApi extends EventEmitter {
778768
);
779769
} catch (e) {
780770
console.error("error searching in the user directory", e);
781-
await this.transport.reply<IWidgetApiErrorResponseData>(request, {
782-
error: { message: "Unexpected error while searching in the user directory" },
783-
});
771+
this.handleDriverError(e, request, "Unexpected error while searching in the user directory");
784772
}
785773
}
786774

@@ -800,9 +788,7 @@ export class ClientWidgetApi extends EventEmitter {
800788
);
801789
} catch (e) {
802790
console.error("error while getting the media configuration", e);
803-
await this.transport.reply<IWidgetApiErrorResponseData>(request, {
804-
error: { message: "Unexpected error while getting the media configuration" },
805-
});
791+
this.handleDriverError(e, request, "Unexpected error while getting the media configuration");
806792
}
807793
}
808794

@@ -822,9 +808,7 @@ export class ClientWidgetApi extends EventEmitter {
822808
);
823809
} catch (e) {
824810
console.error("error while uploading a file", e);
825-
await this.transport.reply<IWidgetApiErrorResponseData>(request, {
826-
error: { message: "Unexpected error while uploading a file" },
827-
});
811+
this.handleDriverError(e, request, "Unexpected error while uploading a file");
828812
}
829813
}
830814

@@ -844,12 +828,20 @@ export class ClientWidgetApi extends EventEmitter {
844828
);
845829
} catch (e) {
846830
console.error("error while downloading a file", e);
847-
this.transport.reply<IWidgetApiErrorResponseData>(request, {
848-
error: { message: "Unexpected error while downloading a file" },
849-
});
831+
this.handleDriverError(e, request, "Unexpected error while downloading a file");
850832
}
851833
}
852834

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

src/WidgetApi.ts

Lines changed: 14 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,19 @@ import {
9595
UpdateDelayedEventAction,
9696
} from "./interfaces/UpdateDelayedEventAction";
9797

98+
export class WidgetApiResponseError extends Error {
99+
static {
100+
this.prototype.name = this.name;
101+
}
102+
103+
public constructor(
104+
message: string,
105+
public readonly data: IWidgetApiErrorResponseDataDetails,
106+
) {
107+
super(message);
108+
}
109+
}
110+
98111
/**
99112
* API handler for widgets. This raises events for each action
100113
* received as `action:${action}` (eg: "action:screenshot").

src/driver/WidgetDriver.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
IRoomEvent,
2323
IRoomAccountData,
2424
ITurnServer,
25+
IWidgetApiErrorResponseDataDetails,
2526
UpdateDelayedEventAction,
2627
} from "..";
2728

@@ -358,4 +359,14 @@ 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 this driver in a format compatible with the Widget API.
365+
* @param error The error to handle.
366+
* @returns The error expressed as a {@link IWidgetApiErrorResponseDataDetails},
367+
* or undefined if it cannot be expressed as one.
368+
*/
369+
public processError(error: unknown): IWidgetApiErrorResponseDataDetails | undefined {
370+
return undefined;
371+
}
361372
}

src/interfaces/IWidgetApiErrorResponse.ts

Lines changed: 30 additions & 8 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,20 +16,42 @@
1616

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

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
35+
}
36+
37+
export interface IWidgetApiErrorResponseDataDetails {
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
40+
}
41+
1942
export interface IWidgetApiErrorResponseData extends IWidgetApiResponseData {
2043
error: {
44+
/** A user-friendly string describing the error */
2145
message: string;
22-
};
46+
} & IWidgetApiErrorResponseDataDetails;
2347
}
2448

2549
export interface IWidgetApiErrorResponse extends IWidgetApiResponse {
2650
response: IWidgetApiErrorResponseData;
2751
}
2852

29-
export function isErrorResponse(responseData: IWidgetApiResponseData): boolean {
30-
if ("error" in responseData) {
31-
const err = <IWidgetApiErrorResponseData>responseData;
32-
return !!err.error.message;
33-
}
34-
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";
3557
}

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 & 4 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,
@@ -194,8 +194,8 @@ export class PostmessageTransport extends EventEmitter implements ITransport {
194194
if (!req) return; // response to an unknown request
195195

196196
if (isErrorResponse(response.response)) {
197-
const err = <IWidgetApiErrorResponseData>response.response;
198-
req.reject(new Error(err.error.message));
197+
const {message, ...data} = response.response.error;
198+
req.reject(new WidgetApiResponseError(message, data));
199199
} else {
200200
req.resolve(response);
201201
}

0 commit comments

Comments
 (0)