From 80aac06ff3ac88a81cc7df61466ddf05e3add4d9 Mon Sep 17 00:00:00 2001 From: Benny Joo Date: Mon, 13 Jan 2025 07:42:00 -0500 Subject: [PATCH] chore: restore error pages for pages router (#18625) --- .../error/404-page.tsx => pages/404.tsx} | 13 ++ apps/web/pages/_error.tsx | 119 ++++++++++++++++-- 2 files changed, 124 insertions(+), 8 deletions(-) rename apps/web/{components/error/404-page.tsx => pages/404.tsx} (97%) diff --git a/apps/web/components/error/404-page.tsx b/apps/web/pages/404.tsx similarity index 97% rename from apps/web/components/error/404-page.tsx rename to apps/web/pages/404.tsx index aa5af61d11..a90e17681f 100644 --- a/apps/web/components/error/404-page.tsx +++ b/apps/web/pages/404.tsx @@ -1,5 +1,6 @@ "use client"; +import type { GetStaticPropsContext } from "next"; import Link from "next/link"; import { usePathname } from "next/navigation"; import { useEffect, useState } from "react"; @@ -15,6 +16,8 @@ import { Icon } from "@calcom/ui"; import PageWrapper from "@components/PageWrapper"; +import { getTranslations } from "@server/lib/getTranslations"; + enum pageType { ORG = "org", TEAM = "team", @@ -244,3 +247,13 @@ export default function Custom404() { } Custom404.PageWrapper = PageWrapper; + +export const getStaticProps = async (context: GetStaticPropsContext) => { + const i18n = await getTranslations(context); + + return { + props: { + i18n, + }, + }; +}; diff --git a/apps/web/pages/_error.tsx b/apps/web/pages/_error.tsx index d5e753d1c7..76089811d6 100644 --- a/apps/web/pages/_error.tsx +++ b/apps/web/pages/_error.tsx @@ -1,12 +1,115 @@ -import Custom404 from "@components/error/404-page"; +/** + * 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"; -const CustomError = () => { - return ; +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 & HttpError) | null; +type CustomErrorProps = { + err?: AugmentedError; + message?: string; + hasGetInitialPropsRun?: boolean; +} & Omit; + +type AugmentedNextPageContext = Omit & { + err: AugmentedError; +}; + +const log = logger.getSubLogger({ prefix: ["[error]"] }); + +const CustomError: NextPage = (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 ; +}; + +/** + * 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); + + res.setHeader("x-pages-router-error", "true"); + + 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; - -// `pages/_error` needs to exist until all routes are -// fully migrated to App Router. Dynamic routes in Pages Router -// like `/[user]/[type]` or `/team/[slug]` cannot use App Router's -// error handling