feat: render custom error page for unexpected sever error + remove pages/_error (#18606)

* remove page/_error

* refactor app/error
This commit is contained in:
Benny Joo
2025-01-12 14:12:02 +01:00
committed by GitHub
parent 62b753afba
commit a3d7bcd22e
3 changed files with 70 additions and 175 deletions
+41 -50
View File
@@ -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}
/>
);
}
+29 -12
View File
@@ -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 well 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>
)}
</>
);
};
-113
View File
@@ -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;