feat: render custom error page for unexpected sever error + remove pages/_error (#18606)
* remove page/_error * refactor app/error
This commit is contained in:
+41
-50
@@ -1,64 +1,55 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* Typescript class based component for custom-error
|
||||
* @link https://nextjs.org/docs/advanced-features/custom-error-page
|
||||
*/
|
||||
import type { NextPage } from "next";
|
||||
import type { ErrorProps } from "next/error";
|
||||
import React from "react";
|
||||
|
||||
import { getErrorFromUnknown } from "@calcom/lib/errors";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { redactError } from "@calcom/lib/redactError";
|
||||
|
||||
import { ErrorPage } from "@components/error/error-page";
|
||||
|
||||
type NextError = Error & { digest?: string };
|
||||
|
||||
// Ref: https://nextjs.org/docs/app/api-reference/file-conventions/error#props
|
||||
export type DefaultErrorProps = {
|
||||
error: NextError;
|
||||
reset: () => void; // A function to reset the error boundary
|
||||
};
|
||||
|
||||
type AugmentedError = NextError | HttpError | null;
|
||||
|
||||
type CustomErrorProps = {
|
||||
err?: AugmentedError;
|
||||
statusCode?: number;
|
||||
message?: string;
|
||||
} & Omit<ErrorProps, "err" | "statusCode">;
|
||||
|
||||
const log = logger.getSubLogger({ prefix: ["[error]"] });
|
||||
|
||||
const CustomError: NextPage<DefaultErrorProps> = (props) => {
|
||||
const { error } = props;
|
||||
let errorObject: CustomErrorProps = {
|
||||
message: error.message,
|
||||
err: error,
|
||||
};
|
||||
|
||||
if (error instanceof HttpError) {
|
||||
const redactedError = redactError(error);
|
||||
errorObject = {
|
||||
statusCode: error.statusCode,
|
||||
title: redactedError.name,
|
||||
message: redactedError.message,
|
||||
err: {
|
||||
...redactedError,
|
||||
...error,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// `error.digest` property contains an automatically generated hash of the error that can be used to match the corresponding error in server-side logs
|
||||
log.debug(`${error?.toString() ?? JSON.stringify(error)}`);
|
||||
log.info("errorObject: ", errorObject);
|
||||
|
||||
return (
|
||||
<ErrorPage statusCode={errorObject.statusCode} error={errorObject.err} message={errorObject.message} />
|
||||
);
|
||||
type ErrorProps = {
|
||||
error: Error;
|
||||
reset: () => void;
|
||||
};
|
||||
|
||||
export default CustomError;
|
||||
export default function Error({ error, reset }: ErrorProps) {
|
||||
React.useEffect(() => {
|
||||
log.error(error);
|
||||
}, [error]);
|
||||
|
||||
const processedError = React.useMemo(() => {
|
||||
const err = getErrorFromUnknown(error);
|
||||
|
||||
if (err instanceof HttpError) {
|
||||
const redactedError = redactError(err);
|
||||
return {
|
||||
statusCode: err.statusCode,
|
||||
title: redactedError.name,
|
||||
name: redactedError.name,
|
||||
message: redactedError.message,
|
||||
url: err.url,
|
||||
method: err.method,
|
||||
cause: err.cause,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
statusCode: 500,
|
||||
title: "Internal Server Error",
|
||||
name: "Internal Server Error",
|
||||
message: "An unexpected error occurred.",
|
||||
};
|
||||
}, [error]);
|
||||
|
||||
return (
|
||||
<ErrorPage
|
||||
statusCode={processedError.statusCode}
|
||||
error={processedError}
|
||||
message={processedError.message}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { Button } from "@calcom/ui";
|
||||
|
||||
type Props = {
|
||||
statusCode?: number | null;
|
||||
@@ -53,21 +56,35 @@ export const ErrorPage: React.FC<Props> = (props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="bg-default min-h-screen px-4">
|
||||
<main className="mx-auto max-w-xl pb-6 pt-16 sm:pt-24">
|
||||
<div className="text-center">
|
||||
<p className="text-emphasis text-sm font-semibold uppercase tracking-wide">{statusCode}</p>
|
||||
<h1 className="text-emphasis mt-2 text-4xl font-extrabold tracking-tight sm:text-5xl">
|
||||
<div className="bg-subtle flex h-screen">
|
||||
<div className="rtl: bg-default m-auto rounded-md p-10 text-right ltr:text-left">
|
||||
<h1 className="font-cal text-emphasis text-6xl">{statusCode}</h1>
|
||||
<h2 className="text-emphasis mt-6 max-w-2xl text-2xl font-medium">It's not you, it's us.</h2>
|
||||
<p className="text-default mb-6 mt-4 max-w-2xl text-sm">
|
||||
Something went wrong on our end. Get in touch with our support team, and we’ll get it fixed right
|
||||
away for you.
|
||||
</p>
|
||||
|
||||
<div className="mb-8 flex flex-col">
|
||||
<p className="text-default mb-4 max-w-2xl text-sm">
|
||||
Please provide the following text when contacting support to better help you:
|
||||
</p>
|
||||
<pre className="bg-emphasis text-emphasis w-full max-w-2xl whitespace-normal break-words rounded-md p-4">
|
||||
{message}
|
||||
</h1>
|
||||
</pre>
|
||||
</div>
|
||||
</main>
|
||||
{displayDebug && (
|
||||
<div className="flex-wrap">
|
||||
<ErrorDebugPanel error={error} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button href="mailto:support@cal.com">Contact Support</Button>
|
||||
<Button color="secondary" href="javascript:history.back()" className="ml-2">
|
||||
Go back
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{displayDebug && (
|
||||
<div className="flex-wrap">
|
||||
<ErrorDebugPanel error={error} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
/**
|
||||
* Typescript class based component for custom-error
|
||||
* @link https://nextjs.org/docs/advanced-features/custom-error-page
|
||||
*/
|
||||
import type { NextPage, NextPageContext } from "next";
|
||||
import type { ErrorProps } from "next/error";
|
||||
import NextError from "next/error";
|
||||
import React from "react";
|
||||
|
||||
import { getErrorFromUnknown } from "@calcom/lib/errors";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { redactError } from "@calcom/lib/redactError";
|
||||
|
||||
import { ErrorPage } from "@components/error/error-page";
|
||||
|
||||
// Adds HttpException to the list of possible error types.
|
||||
type AugmentedError = (NonNullable<NextPageContext["err"]> & HttpError) | null;
|
||||
type CustomErrorProps = {
|
||||
err?: AugmentedError;
|
||||
message?: string;
|
||||
hasGetInitialPropsRun?: boolean;
|
||||
} & Omit<ErrorProps, "err">;
|
||||
|
||||
type AugmentedNextPageContext = Omit<NextPageContext, "err"> & {
|
||||
err: AugmentedError;
|
||||
};
|
||||
|
||||
const log = logger.getSubLogger({ prefix: ["[error]"] });
|
||||
|
||||
const CustomError: NextPage<CustomErrorProps> = (props) => {
|
||||
const { statusCode, err, message, hasGetInitialPropsRun } = props;
|
||||
|
||||
if (!hasGetInitialPropsRun && err) {
|
||||
// getInitialProps is not called in case of
|
||||
// https://github.com/vercel/next.js/issues/8592. As a workaround, we pass
|
||||
// err via _app.tsx so it can be captured
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const e = getErrorFromUnknown(err);
|
||||
// can be captured here
|
||||
// e.g. Sentry.captureException(e);
|
||||
}
|
||||
return <ErrorPage statusCode={statusCode} error={err} message={message} />;
|
||||
};
|
||||
|
||||
/**
|
||||
* Partially adapted from the example in
|
||||
* https://github.com/vercel/next.js/tree/canary/examples/with-sentry
|
||||
*/
|
||||
CustomError.getInitialProps = async (ctx: AugmentedNextPageContext) => {
|
||||
const { res, err, asPath } = ctx;
|
||||
const errorInitialProps = (await NextError.getInitialProps({
|
||||
res,
|
||||
err,
|
||||
} as NextPageContext)) as CustomErrorProps;
|
||||
|
||||
// Workaround for https://github.com/vercel/next.js/issues/8592, mark when
|
||||
// getInitialProps has run
|
||||
errorInitialProps.hasGetInitialPropsRun = true;
|
||||
|
||||
// If a HttpError message, let's override defaults
|
||||
if (err instanceof HttpError) {
|
||||
const redactedError = redactError(err);
|
||||
errorInitialProps.statusCode = err.statusCode;
|
||||
errorInitialProps.title = redactedError.name;
|
||||
errorInitialProps.message = redactedError.message;
|
||||
errorInitialProps.err = {
|
||||
...redactedError,
|
||||
url: err.url,
|
||||
statusCode: err.statusCode,
|
||||
cause: err.cause,
|
||||
method: err.method,
|
||||
};
|
||||
}
|
||||
|
||||
if (res) {
|
||||
// Running on the server, the response object is available.
|
||||
//
|
||||
// Next.js will pass an err on the server if a page's `getInitialProps`
|
||||
// threw or returned a Promise that rejected
|
||||
|
||||
// Overrides http status code if present in errorInitialProps
|
||||
res.statusCode = errorInitialProps.statusCode;
|
||||
|
||||
log.debug(`server side logged this: ${err?.toString() ?? JSON.stringify(err)}`);
|
||||
log.info("return props, ", errorInitialProps);
|
||||
|
||||
return errorInitialProps;
|
||||
} else {
|
||||
// Running on the client (browser).
|
||||
//
|
||||
// Next.js will provide an err if:
|
||||
//
|
||||
// - a page's `getInitialProps` threw or returned a Promise that rejected
|
||||
// - an exception was thrown somewhere in the React lifecycle (render,
|
||||
// componentDidMount, etc) that was caught by Next.js's React Error
|
||||
// Boundary. Read more about what types of exceptions are caught by Error
|
||||
// Boundaries: https://reactjs.org/docs/error-boundaries.html
|
||||
if (err) {
|
||||
log.info("client side logged this", err);
|
||||
return errorInitialProps;
|
||||
}
|
||||
}
|
||||
|
||||
// If this point is reached, getInitialProps was called without any
|
||||
// information about what the error might be. This is unexpected and may
|
||||
// indicate a bug introduced in Next.js
|
||||
new Error(`_error.tsx getInitialProps missing data at path: ${asPath}`);
|
||||
|
||||
return errorInitialProps;
|
||||
};
|
||||
|
||||
export default CustomError;
|
||||
Reference in New Issue
Block a user