Skip to content

Commit

Permalink
Merge pull request #2324 from trilitech/wc_http
Browse files Browse the repository at this point in the history
feat: error handling for HttpErrorResponse
  • Loading branch information
dianasavvatina authored Jan 21, 2025
2 parents 30f1084 + ef6816a commit 26ce960
Show file tree
Hide file tree
Showing 5 changed files with 145 additions and 8 deletions.
6 changes: 3 additions & 3 deletions apps/desktop-e2e/src/helpers/AccountGroup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,14 @@ export class AccountGroupBuilder {

setDerivationPathTemplate(derivationPathTemplate: string): void {
if (this.accountGroup.type !== "mnemonic") {
throw new CustomError(`Derivation path is not used for ${this.accountGroup.type} accounts}`);
throw new CustomError(`Derivation path is not used for ${this.accountGroup.type} accounts`);
}
this.derivationPathTemplate = derivationPathTemplate;
}

setSeedPhrase(seedPhrase: string[]): void {
if (this.accountGroup.type !== "mnemonic") {
throw new CustomError(`Seed phrase is not used for ${this.accountGroup.type} accounts}`);
throw new CustomError(`Seed phrase is not used for ${this.accountGroup.type} accounts`);
}
this.seedPhrase = seedPhrase;
}
Expand All @@ -58,7 +58,7 @@ export class AccountGroupBuilder {

async setSecretKey(secretKey: string, accountIndex = 0): Promise<void> {
if (this.accountGroup.type !== "secret_key") {
throw new CustomError(`Secret key is not used for ${this.accountGroup.type} accounts}`);
throw new CustomError(`Secret key is not used for ${this.accountGroup.type} accounts`);
}
this.accountGroup.accounts[accountIndex].pkh = await (
await InMemorySigner.fromSecretKey(secretKey)
Expand Down
4 changes: 3 additions & 1 deletion packages/utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"@types/eslint": "^8",
"@types/jest": "^29.5.14",
"@types/lodash": "^4",
"@types/sanitize-html": "^2.13.0",
"@umami/eslint-config": "workspace:^",
"@umami/jest-config": "workspace:^",
"@umami/test-utils": "workspace:^",
Expand Down Expand Up @@ -60,6 +61,7 @@
},
"dependencies": {
"bignumber.js": "^9.1.2",
"lodash": "^4.17.21"
"lodash": "^4.17.21",
"sanitize-html": "^2.14.0"
}
}
40 changes: 38 additions & 2 deletions packages/utils/src/ErrorContext.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
WalletConnectError,
WcErrorCode,
getErrorContext,
getHttpErrorMessage,
getTezErrorMessage,
getWcErrorResponse,
} from "./ErrorContext";
Expand Down Expand Up @@ -62,6 +63,7 @@ describe("getErrorContext", () => {
expect(context.stacktrace).toBeDefined();
expect(context.timestamp).toBeDefined();
});

it("should handle WalletConnectError instances", () => {
const error = new WalletConnectError(
"Custom WC error message",
Expand All @@ -76,6 +78,41 @@ describe("getErrorContext", () => {
expect(context.stacktrace).toBeDefined();
expect(context.timestamp).toBeDefined();
});

it("should handle HttpErrorResponse instances", () => {
const error = {
status: 503,
message:
"Http error response: (503) <html><head><title>503 Service Temporarily Unavailable</title></head>",
url: "https://example.com/api",
};

const context = getErrorContext(error);
expect(context.description).toBe(
"HTTP request failed for https://example.com/api (503) Service Unavailable - The server is temporarily unable to handle the request. Please try again later or contact support."
);
expect(context.code).toBe(503);
expect(context.technicalDetails).toEqual([
503,
"Service Unavailable - The server is temporarily unable to handle the request. Please try again later or contact support.",
"https://example.com/api",
"Http error response: (503) 503 Service Temporarily Unavailable",
]);
expect(context.stacktrace).toBeDefined();
expect(context.timestamp).toBeDefined();
});

it("should recognize well known http error codes", () => {
expect(getHttpErrorMessage(123)).toEqual(
"Unknown Error - Status code: 123. Please try again later or contact support."
);
for (const status of [400, 401, 403, 404, 405, 408, 409, 410, 500, 501, 502, 503, 504]) {
expect(getHttpErrorMessage(status)).toBeDefined();
expect(getHttpErrorMessage(status)).not.toEqual(
"Unknown Error - Status code: 123. Please try again later or contact support."
);
}
});
});

describe("getTezErrorMessage", () => {
Expand Down Expand Up @@ -141,7 +178,6 @@ describe("getTezErrorMessage", () => {
});

it("should return TezosOperationError message", () => {
// const error = new TezosOperationError(errors:[], lastError: { id: 'michelson_v1.script_rejected', with: { prim: 'Unit' } });
const mockError: TezosOperationErrorWithMessage = {
kind: "temporary",
id: "proto.020-PsParisC.michelson_v1.script_rejected",
Expand All @@ -154,7 +190,7 @@ describe("getTezErrorMessage", () => {
);
const context = getErrorContext(error);
expect(context.description).toContain(
"Rejected by chain. The contract code failed to run. Please check the contract. Details: Fail entrypoint"
"Rejected by chain. The contract code failed to run. Please check the contract.\nDetails: Fail entrypoint"
);
expect(context.technicalDetails).toEqual([
"proto.020-PsParisC.michelson_v1.script_rejected",
Expand Down
57 changes: 55 additions & 2 deletions packages/utils/src/ErrorContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { type MichelsonV1ExpressionBase, type TezosGenericOperationError } from
import { TezosOperationError, type TezosOperationErrorWithMessage } from "@taquito/taquito";
import { type ErrorResponse } from "@walletconnect/jsonrpc-utils";
import { type SessionTypes } from "@walletconnect/types";
import sanitizeHtml from "sanitize-html";

import { TezosRpcErrors } from "./TezosRpcErrors";

Expand Down Expand Up @@ -88,6 +89,40 @@ export const getTezErrorMessage = (err: string): string | undefined => {
}
};

export const getHttpErrorMessage = (status: number): string => {
const defaultAction = "Please try again later or contact support.";
const httpErrorDescriptions: { [key: number]: string } = {
400: "Bad Request - The server could not understand the request. Please check your input and try again.",
401: "Unauthorized - Authentication is required or has failed. Please log in and try again.",
403: "Forbidden - You do not have permission to access the requested resource. Contact support if you believe this is an error.",
404: `Not Found - The requested resource could not be found. ${defaultAction}`,
405: `Method Not Allowed - The HTTP method is not supported by the resource. ${defaultAction}`,
408: "Request Timeout - The server timed out waiting for the request. Please check your network connection and try again.",
409: `Conflict - There is a conflict with the current state of the resource. ${defaultAction}`,
410: `Gone - The resource is no longer available. It may have been removed or retired. ${defaultAction}`,
500: `Internal Server Error - An unexpected error occurred on the server. ${defaultAction}`,
501: "Not Implemented - The server does not support the functionality required to fulfill the request. Contact support for assistance.",
502: "Bad Gateway - The server received an invalid response from the upstream server. Please try again later.",
503: `Service Unavailable - The server is temporarily unable to handle the request. ${defaultAction}`,
504: "Gateway Timeout - The server did not receive a timely response from the upstream server. Check your network and try again.",
};

return (
httpErrorDescriptions[status] ||
`Unknown Error - Status code: ${status}. Please try again later or contact support.`
);
};

function stripHtmlTags(html: string): string {
return sanitizeHtml(html, {
allowedTags: [],
allowedAttributes: {},
})
.replace(/[\r\n]/g, " ") // replace new lines with spaces
.replace(/\s+/g, " ") // replace multiple spaces with single space
.trim();
}

const isTezosOperationErrorWithMessage = (
error: TezosGenericOperationError
): error is TezosOperationErrorWithMessage => "with" in error;
Expand All @@ -97,7 +132,7 @@ export const getErrorContext = (error: any, silent: boolean = false): ErrorConte
"Something went wrong. Please try again. Contact support if the issue persists.";
let description = defaultDescription;
let technicalDetails: any = undefined;
let code = WcErrorCode.INTERNAL_ERROR;
let code: number = WcErrorCode.INTERNAL_ERROR;
const errorMessage = typeof error === "string" ? error : error.message;

let stacktrace = "";
Expand All @@ -119,14 +154,32 @@ export const getErrorContext = (error: any, silent: boolean = false): ErrorConte
description =
"Rejected by chain. " +
(getTezErrorMessage(lastError.id) ?? "") +
" Details: " +
"\nDetails: " +
errorMessage;
if (isTezosOperationErrorWithMessage(lastError)) {
const failswith: MichelsonV1ExpressionBase = lastError.with;
technicalDetails = [lastError.id, { with: failswith }];
} else {
technicalDetails = [lastError.id];
}
} else if (
typeof error === "object" &&
error !== null &&
"status" in error &&
typeof error.status === "number" &&
"url" in error &&
typeof error.url === "string"
) {
// HttpErrorResponse exception has these fields. Since we need the fields only, we can use them directly.
const httpError = getHttpErrorMessage(error.status);
const plainMessage = stripHtmlTags(error.message);
description = `HTTP request failed for ${error.url} (${error.status}) ${httpError}`;
code = error.status;
console.log("HTTP ERROR", error);
if (code === 500) {
description = `${description}\nDetails: ${plainMessage}`;
}
technicalDetails = [error.status, httpError, error.url, plainMessage];
} else if (error instanceof Error || Object.prototype.hasOwnProperty.call(error, "message")) {
description = getTezErrorMessage(errorMessage) ?? defaultDescription;
technicalDetails = errorMessage;
Expand Down
46 changes: 46 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 comment on commit 26ce960

@github-actions
Copy link

Choose a reason for hiding this comment

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

Title Lines Statements Branches Functions
apps/desktop Coverage: 83%
83.81% (1787/2132) 79.58% (850/1068) 78.27% (454/580)
apps/web Coverage: 83%
83.81% (1787/2132) 79.58% (850/1068) 78.27% (454/580)
packages/components Coverage: 97%
97.51% (196/201) 95.91% (94/98) 88.13% (52/59)
packages/core Coverage: 81%
82.37% (215/261) 72.51% (95/131) 81.66% (49/60)
packages/crypto Coverage: 100%
100% (43/43) 90.9% (10/11) 100% (7/7)
packages/data-polling Coverage: 96%
94.63% (141/149) 87.5% (21/24) 92.85% (39/42)
packages/multisig Coverage: 98%
98.47% (129/131) 85.71% (18/21) 100% (36/36)
packages/social-auth Coverage: 100%
100% (21/21) 100% (11/11) 100% (3/3)
packages/state Coverage: 85%
84.48% (828/980) 81.35% (192/236) 77.83% (302/388)
packages/tezos Coverage: 89%
88.72% (118/133) 94.59% (35/37) 86.84% (33/38)
packages/tzkt Coverage: 89%
87.32% (62/71) 87.5% (14/16) 80.48% (33/41)

Please sign in to comment.