Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pass HTTP status code & errcode from CS-API errors #100

Merged
merged 12 commits into from
Nov 8, 2024
3 changes: 3 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ module.exports = {
"files": ["src/**/*.ts", "test/**/*.ts"],
"extends": ["matrix-org/ts"],
"rules": {
// TypeScript has its own version of this
"babel/no-invalid-this": "off",

"quotes": "off",
},
}],
Expand Down
54 changes: 23 additions & 31 deletions src/ClientWidgetApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -330,15 +330,13 @@
});
}

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

try {
this.driver.navigate(request.data.uri.toString()).catch(e => onErr(e)).then(() => {
this.driver.navigate(request.data.uri.toString()).catch((e: unknown) => onErr(e)).then(() => {
return this.transport.reply<IWidgetApiAcknowledgeResponseData>(request, {});
});
} catch (e) {
Expand Down Expand Up @@ -437,7 +435,7 @@
if (request.data.room_ids) {
askRoomIds = request.data.room_ids as string[];
if (!Array.isArray(askRoomIds)) {
askRoomIds = [askRoomIds as any as string];

Check warning on line 438 in src/ClientWidgetApi.ts

View workflow job for this annotation

GitHub Actions / build

Unexpected any. Specify a different type
}
for (const roomId of askRoomIds) {
if (!this.canUseRoomTimeline(roomId)) {
Expand Down Expand Up @@ -554,11 +552,9 @@
delay_id: sentEvent.delayId,
}),
});
}).catch(e => {
}).catch((e: unknown) => {
console.error("error sending event: ", e);
return this.transport.reply<IWidgetApiErrorResponseData>(request, {
error: {message: "Error sending event"},
});
this.handleDriverError(e, request, "Error sending event");
});
}

Expand All @@ -581,11 +577,9 @@
case UpdateDelayedEventAction.Send:
this.driver.updateDelayedEvent(request.data.delay_id, request.data.action).then(() => {
return this.transport.reply<IWidgetApiAcknowledgeResponseData>(request, {});
}).catch(e => {
}).catch((e: unknown) => {
console.error("error updating delayed event: ", e);
return this.transport.reply<IWidgetApiErrorResponseData>(request, {
error: {message: "Error updating delayed event"},
});
this.handleDriverError(e, request, "Error updating delayed event");
});
break;
default:
Expand Down Expand Up @@ -618,9 +612,7 @@
await this.transport.reply<ISendToDeviceFromWidgetResponseData>(request, {});
} catch (e) {
console.error("error sending to-device event", e);
await this.transport.reply<IWidgetApiErrorResponseData>(request, {
error: {message: "Error sending event"},
});
this.handleDriverError(e, request, "Error sending event");
}
}
}
Expand Down Expand Up @@ -735,9 +727,7 @@
);
} catch (e) {
console.error("error getting the relations", e);
await this.transport.reply<IWidgetApiErrorResponseData>(request, {
error: { message: "Unexpected error while reading relations" },
});
this.handleDriverError(e, request, "Unexpected error while reading relations");
}
}

Expand Down Expand Up @@ -778,9 +768,7 @@
);
} catch (e) {
console.error("error searching in the user directory", e);
await this.transport.reply<IWidgetApiErrorResponseData>(request, {
error: { message: "Unexpected error while searching in the user directory" },
});
this.handleDriverError(e, request, "Unexpected error while searching in the user directory");
}
}

Expand All @@ -800,9 +788,7 @@
);
} catch (e) {
console.error("error while getting the media configuration", e);
await this.transport.reply<IWidgetApiErrorResponseData>(request, {
error: { message: "Unexpected error while getting the media configuration" },
});
this.handleDriverError(e, request, "Unexpected error while getting the media configuration");
}
}

Expand All @@ -822,9 +808,7 @@
);
} catch (e) {
console.error("error while uploading a file", e);
await this.transport.reply<IWidgetApiErrorResponseData>(request, {
error: { message: "Unexpected error while uploading a file" },
});
this.handleDriverError(e, request, "Unexpected error while uploading a file");
}
}

Expand All @@ -844,12 +828,20 @@
);
} catch (e) {
console.error("error while downloading a file", e);
this.transport.reply<IWidgetApiErrorResponseData>(request, {
error: { message: "Unexpected error while downloading a file" },
});
this.handleDriverError(e, request, "Unexpected error while downloading a file");
}
}

private handleDriverError(e: unknown, request: IWidgetApiRequest, message: string) {
const data = this.driver.processError(e);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not a fan of making driver implementations responsible for serializing errors. I'd much rather have it be done by something akin to a method on the error object itself, but that's not simple because errors are of an unknown type here. For now, the current solution works, but I'm open to suggestions.

this.transport.reply<IWidgetApiErrorResponseData>(request, {
error: {
message,
...data,
},
});
}

private handleMessage(ev: CustomEvent<IWidgetApiRequest>) {
if (this.isStopped) return;
const actionEv = new CustomEvent(`action:${ev.detail.action}`, {
Expand Down
15 changes: 14 additions & 1 deletion src/WidgetApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import {
import { ITransport } from "./transport/ITransport";
import { PostmessageTransport } from "./transport/PostmessageTransport";
import { WidgetApiFromWidgetAction, WidgetApiToWidgetAction } from "./interfaces/WidgetApiAction";
import { IWidgetApiErrorResponseData } from "./interfaces/IWidgetApiErrorResponse";
import { IWidgetApiErrorResponseData, IWidgetApiErrorResponseDataDetails } from "./interfaces/IWidgetApiErrorResponse";
import { IStickerActionRequestData } from "./interfaces/StickerAction";
import { IStickyActionRequestData, IStickyActionResponseData } from "./interfaces/StickyAction";
import {
Expand Down Expand Up @@ -95,6 +95,19 @@ import {
UpdateDelayedEventAction,
} from "./interfaces/UpdateDelayedEventAction";

export class WidgetApiResponseError extends Error {
static {
this.prototype.name = this.name;
}

public constructor(
message: string,
public readonly data: IWidgetApiErrorResponseDataDetails,
) {
super(message);
}
}

AndrewFerr marked this conversation as resolved.
Show resolved Hide resolved
/**
* API handler for widgets. This raises events for each action
* received as `action:${action}` (eg: "action:screenshot").
Expand Down
11 changes: 11 additions & 0 deletions src/driver/WidgetDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
IRoomEvent,
IRoomAccountData,
ITurnServer,
IWidgetApiErrorResponseDataDetails,
UpdateDelayedEventAction,
} from "..";

Expand Down Expand Up @@ -358,4 +359,14 @@ export abstract class WidgetDriver {
): Promise<{ file: XMLHttpRequestBodyInit }> {
throw new Error("Download file is not implemented");
}

/**
* Expresses an error thrown by this driver in a format compatible with the Widget API.
* @param error The error to handle.
* @returns The error expressed as a {@link IWidgetApiErrorResponseDataDetails},
* or undefined if it cannot be expressed as one.
*/
public processError(error: unknown): IWidgetApiErrorResponseDataDetails | undefined {
return undefined;
}
AndrewFerr marked this conversation as resolved.
Show resolved Hide resolved
}
38 changes: 30 additions & 8 deletions src/interfaces/IWidgetApiErrorResponse.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
* Copyright 2020 - 2024 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -16,20 +16,42 @@

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

/**
* The format of errors returned by Matrix API requests
* made by a WidgetDriver.
*/
export interface IMatrixApiError {
/** The HTTP status code of the associated request. */
http_status: number; // eslint-disable-line camelcase
/** Any HTTP response headers that are relevant to the error. */
http_headers: {[name: string]: string}; // eslint-disable-line camelcase
/** The URL of the failed request. */
url: string;
/** @see {@link https://spec.matrix.org/latest/client-server-api/#standard-error-response} */
response: {
errcode: string;
error: string;
} & IWidgetApiResponseData; // extensible
}

export interface IWidgetApiErrorResponseDataDetails {
/** Set if the error came from a Matrix API request made by a widget driver */
matrix_api_error?: IMatrixApiError; // eslint-disable-line camelcase
}

AndrewFerr marked this conversation as resolved.
Show resolved Hide resolved
export interface IWidgetApiErrorResponseData extends IWidgetApiResponseData {
error: {
/** A user-friendly string describing the error */
message: string;
};
} & IWidgetApiErrorResponseDataDetails;
}

export interface IWidgetApiErrorResponse extends IWidgetApiResponse {
response: IWidgetApiErrorResponseData;
}

export function isErrorResponse(responseData: IWidgetApiResponseData): boolean {
if ("error" in responseData) {
const err = <IWidgetApiErrorResponseData>responseData;
return !!err.error.message;
}
return false;
export function isErrorResponse(responseData: IWidgetApiResponseData): responseData is IWidgetApiErrorResponseData {
const error = responseData.error;
return typeof error === "object" && error !== null &&
"message" in error && typeof error.message === "string";
}
20 changes: 11 additions & 9 deletions src/transport/ITransport.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
* Copyright 2020 - 2024 The Matrix.org Foundation C.I.C.
AndrewFerr marked this conversation as resolved.
Show resolved Hide resolved
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -71,11 +71,12 @@ export interface ITransport extends EventEmitter {

/**
* Sends a request to the remote end.
* @param {WidgetApiAction} action The action to send.
* @param {IWidgetApiRequestData} data The request data.
* @returns {Promise<IWidgetApiResponseData>} A promise which resolves
* to the remote end's response, or throws with an Error if the request
* failed.
* @param action The action to send.
* @param data The request data.
* @returns A promise which resolves to the remote end's response.
* @throws {Error} if the request failed with a generic error.
* @throws {WidgetApiResponseError} if the request failed with error details
* that can be communicated to the Widget API.
*/
send<T extends IWidgetApiRequestData, R extends IWidgetApiResponseData = IWidgetApiAcknowledgeResponseData>(
action: WidgetApiAction,
Expand All @@ -88,9 +89,10 @@ export interface ITransport extends EventEmitter {
* data.
* @param {WidgetApiAction} action The action to send.
* @param {IWidgetApiRequestData} data The request data.
* @returns {Promise<IWidgetApiResponseData>} A promise which resolves
* to the remote end's response, or throws with an Error if the request
* failed.
* @returns {Promise<IWidgetApiResponseData>} A promise which resolves to the remote end's response
* @throws {Error} if the request failed with a generic error.
* @throws {WidgetApiResponseError} if the request failed with error details
* that can be communicated to the Widget API.
*/
sendComplete<T extends IWidgetApiRequestData, R extends IWidgetApiResponse>(action: WidgetApiAction, data: T)
: Promise<R>;
Expand Down
8 changes: 4 additions & 4 deletions src/transport/PostmessageTransport.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
* Copyright 2020 - 2024 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -19,11 +19,11 @@ import { ITransport } from "./ITransport";
import {
invertedDirection,
isErrorResponse,
IWidgetApiErrorResponseData,
IWidgetApiRequest,
IWidgetApiRequestData,
IWidgetApiResponse,
IWidgetApiResponseData,
WidgetApiResponseError,
WidgetApiAction,
WidgetApiDirection,
WidgetApiToWidgetAction,
Expand Down Expand Up @@ -194,8 +194,8 @@ export class PostmessageTransport extends EventEmitter implements ITransport {
if (!req) return; // response to an unknown request

if (isErrorResponse(response.response)) {
const err = <IWidgetApiErrorResponseData>response.response;
req.reject(new Error(err.error.message));
const {message, ...data} = response.response.error;
req.reject(new WidgetApiResponseError(message, data));
} else {
req.resolve(response);
}
Expand Down
Loading
Loading