Skip to content

Commit

Permalink
feat: error handling for HttpErrorResponse
Browse files Browse the repository at this point in the history
  • Loading branch information
dianasavvatina committed Jan 15, 2025
1 parent 5085be1 commit 65bb8aa
Show file tree
Hide file tree
Showing 5 changed files with 95 additions and 17 deletions.
15 changes: 1 addition & 14 deletions apps/web/src/components/WalletConnect/WalletConnectProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import type EventEmitter from "events";
import { type NetworkType } from "@airgap/beacon-wallet";
import { useToast } from "@chakra-ui/react";
import { type WalletKitTypes } from "@reown/walletkit";
import { TezosOperationError } from "@taquito/taquito";
import { useDynamicModalContext } from "@umami/components";
import {
createWalletKit,
Expand All @@ -13,7 +12,7 @@ import {
walletKit,
} from "@umami/state";
import { type Network } from "@umami/tezos";
import { CustomError, WalletConnectError, type WcErrorKey, getWcErrorResponse } from "@umami/utils";
import { CustomError, WalletConnectError, getWcErrorResponse } from "@umami/utils";
import { formatJsonRpcError } from "@walletconnect/jsonrpc-utils";
import { type SessionTypes } from "@walletconnect/types";
import { getSdkError } from "@walletconnect/utils";
Expand Down Expand Up @@ -106,19 +105,7 @@ export const WalletConnectProvider = ({ children }: PropsWithChildren) => {
await handleWcRequest(event, session);
}).catch(async error => {
const { id, topic } = event;
let wcErrorKey: WcErrorKey = "UNKNOWN_ERROR";

if (error instanceof WalletConnectError) {
wcErrorKey = error.wcError;
} else if (error instanceof TezosOperationError) {
wcErrorKey = "REJECTED_BY_CHAIN";
}
const response = formatJsonRpcError(id, getWcErrorResponse(error));
if (wcErrorKey === "USER_REJECTED") {
console.info("WC request rejected", wcErrorKey, event, error);
} else {
console.warn("WC request failed", wcErrorKey, event, error, response);
}
// dApp is waiting so we need to notify it
await walletKit.respondSessionRequest({ topic, response });
}),
Expand Down
3 changes: 2 additions & 1 deletion packages/data-polling/src/useReactQueryErrorHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@ export const useReactQueryErrorHandler = () => {
return;
}
dispatch(errorsActions.add(getErrorContext(error)));
const context = getErrorContext(error);

if (!toast.isActive(toastId)) {
toast({
id: toastId,
description: `Data fetching error: ${error.message}`,
description: `Data fetching error: ${context.description}`,
status: "error",
isClosable: true,
duration: 10000,
Expand Down
2 changes: 1 addition & 1 deletion packages/state/src/hooks/useAsyncActionHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export const useAsyncActionHandler = () => {
try {
return await fn();
} catch (error: any) {
const errorContext = getErrorContext(error);
const errorContext = getErrorContext(error, true);

// Prevent double toast and record of the same error if case of nested handleAsyncActionUnsafe calls.
// Still we need to re-throw the error to propagate it to the caller.
Expand Down
38 changes: 38 additions & 0 deletions packages/utils/src/ErrorContext.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { TezosOperationError, type TezosOperationErrorWithMessage } from "@taqui
import {
CustomError,
WalletConnectError,
explainHttpError,
explainTezError,
getErrorContext,
getWcErrorResponse,
Expand Down Expand Up @@ -61,6 +62,7 @@ describe("getErrorContext", () => {
expect(context.stacktrace).toBeDefined();
expect(context.timestamp).toBeDefined();
});

it("should handle WalletConnectError instances", () => {
const error = new WalletConnectError("Custom WC error message", "INTERNAL_ERROR", null);

Expand All @@ -71,6 +73,42 @@ describe("getErrorContext", () => {
expect(context.stacktrace).toBeDefined();
expect(context.timestamp).toBeDefined();
});

it("should handle HttpErrorResponse instances", () => {
const error = {
status: 503,
statusText: "",
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",
]);
expect(context.stacktrace).toBeDefined();
expect(context.timestamp).toBeDefined();
});

it("should recognize well known http error codes", () => {
expect(explainHttpError(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(explainHttpError(status)).toBeDefined();
expect(explainHttpError(status)).not.toEqual(
"Unknown Error - Status code: 123. Please try again later or contact support."
);
}
});
});

describe("explainTezError", () => {
Expand Down
54 changes: 53 additions & 1 deletion packages/utils/src/ErrorContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,43 @@ export const explainTezError = (err: string): string | undefined => {
}
};

export const explainHttpError = (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 html
.replace(/<[^>]*>/g, "") // remove tags

Check failure

Code scanning / CodeQL

Polynomial regular expression used on uncontrolled data High

This
regular expression
that depends on
library input
may run slow on strings starting with '<' and with many repetitions of '<'.
This
regular expression
that depends on
library input
may run slow on strings starting with '<' and with many repetitions of '<'.

Check failure

Code scanning / CodeQL

Incomplete multi-character sanitization High

This string may still contain
<script
, which may cause an HTML element injection vulnerability.
.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;

export const getErrorContext = (error: any): ErrorContext => {
export const getErrorContext = (error: any, silent: boolean = false): ErrorContext => {
const defaultDescription =
"Something went wrong. Please try again. Contact support if the issue persists.";
let description = defaultDescription;
Expand Down Expand Up @@ -119,6 +151,22 @@ export const getErrorContext = (error: any): ErrorContext => {
} else {
technicalDetails = [lastError.id];
}
} else if (
typeof error === "object" &&
"status" in error &&
"statusText" in error &&
"url" in error
) {
// HttpErrorResponse is defined in @angular. Too heavy for what we need.
const httpError = explainHttpError(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} Details: ${plainMessage}`;
}
technicalDetails = [error.status, httpError, error.statusText, error.url, plainMessage];
} else if (error instanceof Error || Object.prototype.hasOwnProperty.call(error, "message")) {
const explanation = explainTezError(errorMessage);
if (explanation) {
Expand All @@ -129,6 +177,10 @@ export const getErrorContext = (error: any): ErrorContext => {
technicalDetails = errorMessage;
}

if (!silent) {
console.warn("Request failed", code, description, technicalDetails, error);
}

return {
timestamp: new Date().toISOString(),
description,
Expand Down

0 comments on commit 65bb8aa

Please sign in to comment.