feat: add better error handling (#605)
* feat: add better error handling * refactor: update after review * refactor: remove unnecessary code * refactor: better path structure * refactor: fetch-wrapper after code review Co-authored-by: Mihai Colceriu <colceriumi@gmail.com>
This commit is contained in:
parent
70f595ec08
commit
903f7729c7
18 changed files with 410 additions and 228 deletions
72
components/error/error-page.tsx
Normal file
72
components/error/error-page.tsx
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
import React from "react";
|
||||||
|
import { HttpError } from "@lib/core/http/error";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
statusCode?: number | null;
|
||||||
|
error?: Error | HttpError | null;
|
||||||
|
message?: string;
|
||||||
|
/** Display debugging information */
|
||||||
|
displayDebug?: boolean;
|
||||||
|
children?: never;
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultProps = {
|
||||||
|
displayDebug: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const ErrorDebugPanel: React.FC<{ error: Props["error"]; children?: never }> = (props) => {
|
||||||
|
const { error: e } = props;
|
||||||
|
|
||||||
|
const debugMap = [
|
||||||
|
["error.message", e?.message],
|
||||||
|
["error.name", e?.name],
|
||||||
|
["error.class", e instanceof Error ? e.constructor.name : undefined],
|
||||||
|
["http.url", e instanceof HttpError ? e.url : undefined],
|
||||||
|
["http.status", e instanceof HttpError ? e.statusCode : undefined],
|
||||||
|
["http.cause", e instanceof HttpError ? e.cause?.message : undefined],
|
||||||
|
["error.stack", e instanceof Error ? e.stack : undefined],
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-white shadow overflow-hidden sm:rounded-lg">
|
||||||
|
<div className="border-t border-gray-200 px-4 py-5 sm:p-0">
|
||||||
|
<dl className="sm:divide-y sm:divide-gray-200">
|
||||||
|
{debugMap.map(([key, value]) => {
|
||||||
|
if (value !== undefined) {
|
||||||
|
return (
|
||||||
|
<div key={key} className="py-4 sm:py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
|
||||||
|
<dt className="text-sm font-bold text-black">{key}</dt>
|
||||||
|
<dd className="mt-1 text-sm text-black sm:mt-0 sm:col-span-2">{value}</dd>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ErrorPage: React.FC<Props> = (props) => {
|
||||||
|
const { message, statusCode, error, displayDebug } = { ...defaultProps, ...props };
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="bg-white min-h-screen px-4">
|
||||||
|
<main className="max-w-xl mx-auto pb-6 pt-16 sm:pt-24">
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-sm font-semibold text-black uppercase tracking-wide">{statusCode}</p>
|
||||||
|
<h1 className="mt-2 text-4xl font-extrabold text-gray-900 tracking-tight sm:text-5xl">
|
||||||
|
{message}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
{displayDebug && (
|
||||||
|
<div className="flex-wrap">
|
||||||
|
<ErrorDebugPanel error={error} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
33
lib/core/http/error/http-error.ts
Normal file
33
lib/core/http/error/http-error.ts
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
export class HttpError<TCode extends number = number> extends Error {
|
||||||
|
public readonly cause: unknown;
|
||||||
|
public readonly statusCode: TCode;
|
||||||
|
public readonly message: string;
|
||||||
|
public readonly url: string | undefined;
|
||||||
|
public readonly method: string | undefined;
|
||||||
|
|
||||||
|
constructor(opts: { url?: string; method?: string; message?: string; statusCode: TCode; cause?: unknown }) {
|
||||||
|
super(opts.message ?? `HTTP Error ${opts.statusCode} `);
|
||||||
|
|
||||||
|
Object.setPrototypeOf(this, HttpError.prototype);
|
||||||
|
this.name = HttpError.prototype.constructor.name;
|
||||||
|
|
||||||
|
this.cause = opts.cause;
|
||||||
|
this.statusCode = opts.statusCode;
|
||||||
|
this.url = opts.url;
|
||||||
|
this.method = opts.method;
|
||||||
|
this.message = opts.message ?? `HTTP Error ${opts.statusCode}`;
|
||||||
|
|
||||||
|
if (opts.cause instanceof Error && opts.cause.stack) {
|
||||||
|
this.stack = opts.cause.stack;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static fromRequest(request: Request, response: Response) {
|
||||||
|
return new HttpError({
|
||||||
|
message: response.statusText,
|
||||||
|
url: response.url,
|
||||||
|
method: request.method,
|
||||||
|
statusCode: response.status,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
2
lib/core/http/error/index.ts
Normal file
2
lib/core/http/error/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
// Base http Error
|
||||||
|
export { HttpError } from "./http-error";
|
58
lib/core/http/fetch-wrapper.ts
Normal file
58
lib/core/http/fetch-wrapper.ts
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
import { HttpError } from "@lib/core/http/error";
|
||||||
|
|
||||||
|
async function http<T>(path: string, config: RequestInit): Promise<T> {
|
||||||
|
const request = new Request(path, config);
|
||||||
|
const response: Response = await fetch(request);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const err = HttpError.fromRequest(request, response);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
// may error if there is no body, return empty array
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function get<T>(path: string, config?: RequestInit): Promise<T> {
|
||||||
|
const init = { method: "GET", ...config };
|
||||||
|
return await http<T>(path, init);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function post<T, U>(path: string, body: T, config?: RequestInit): Promise<U> {
|
||||||
|
const init = {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
...config,
|
||||||
|
};
|
||||||
|
return await http<U>(path, init);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function put<T, U>(path: string, body: T, config?: RequestInit): Promise<U> {
|
||||||
|
const init = {
|
||||||
|
method: "PUT",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
...config,
|
||||||
|
};
|
||||||
|
return await http<U>(path, init);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function patch<T, U>(path: string, body: T, config?: RequestInit): Promise<U> {
|
||||||
|
const init = {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
...config,
|
||||||
|
};
|
||||||
|
return await http<U>(path, init);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function remove<T, U>(path: string, body: T, config?: RequestInit): Promise<U> {
|
||||||
|
const init = {
|
||||||
|
method: "DELETE",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
...config,
|
||||||
|
};
|
||||||
|
return await http<U>(path, init);
|
||||||
|
}
|
|
@ -1,19 +1,10 @@
|
||||||
|
import * as fetch from "@lib/core/http/fetch-wrapper";
|
||||||
import { CreateEventType } from "@lib/types/event-type";
|
import { CreateEventType } from "@lib/types/event-type";
|
||||||
|
import { EventType } from "@prisma/client";
|
||||||
|
|
||||||
const createEventType = async (data: CreateEventType) => {
|
const createEventType = async (data: CreateEventType) => {
|
||||||
const response = await fetch("/api/availability/eventtype", {
|
const response = await fetch.post<CreateEventType, EventType>("/api/availability/eventtype", data);
|
||||||
method: "POST",
|
return response;
|
||||||
body: JSON.stringify(data),
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(response.statusText);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.json();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default createEventType;
|
export default createEventType;
|
||||||
|
|
|
@ -1,17 +1,11 @@
|
||||||
|
import * as fetch from "@lib/core/http/fetch-wrapper";
|
||||||
|
|
||||||
const deleteEventType = async (data: { id: number }) => {
|
const deleteEventType = async (data: { id: number }) => {
|
||||||
const response = await fetch("/api/availability/eventtype", {
|
const response = await fetch.remove<{ id: number }, Record<string, never>>(
|
||||||
method: "DELETE",
|
"/api/availability/eventtype",
|
||||||
body: JSON.stringify(data),
|
data
|
||||||
headers: {
|
);
|
||||||
"Content-Type": "application/json",
|
return response;
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(response.statusText);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.json();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default deleteEventType;
|
export default deleteEventType;
|
||||||
|
|
|
@ -1,19 +1,10 @@
|
||||||
|
import * as fetch from "@lib/core/http/fetch-wrapper";
|
||||||
import { EventTypeInput } from "@lib/types/event-type";
|
import { EventTypeInput } from "@lib/types/event-type";
|
||||||
|
import { EventType } from "@prisma/client";
|
||||||
|
|
||||||
const updateEventType = async (data: EventTypeInput) => {
|
const updateEventType = async (data: EventTypeInput) => {
|
||||||
const response = await fetch("/api/availability/eventtype", {
|
const response = await fetch.patch<EventTypeInput, EventType>("/api/availability/eventtype", data);
|
||||||
method: "PATCH",
|
return response;
|
||||||
body: JSON.stringify(data),
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(response.statusText);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.json();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default updateEventType;
|
export default updateEventType;
|
||||||
|
|
|
@ -49,6 +49,15 @@ module.exports = withTM({
|
||||||
typescript: {
|
typescript: {
|
||||||
ignoreBuildErrors: true,
|
ignoreBuildErrors: true,
|
||||||
},
|
},
|
||||||
|
webpack: (config) => {
|
||||||
|
config.resolve.fallback = {
|
||||||
|
...config.resolve.fallback, // if you miss it, all the other options in fallback, specified
|
||||||
|
// by next.js will be dropped. Doesn't make much sense, but how it is
|
||||||
|
fs: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
return config;
|
||||||
|
},
|
||||||
async redirects() {
|
async redirects() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
|
|
@ -4,7 +4,7 @@ import type { AppProps as NextAppProps } from "next/app";
|
||||||
import { DefaultSeo } from "next-seo";
|
import { DefaultSeo } from "next-seo";
|
||||||
import { seoConfig } from "@lib/config/next-seo.config";
|
import { seoConfig } from "@lib/config/next-seo.config";
|
||||||
|
|
||||||
// Workaround for https://github.com/zeit/next.js/issues/8592
|
// Workaround for https://github.com/vercel/next.js/issues/8592
|
||||||
export type AppProps = NextAppProps & {
|
export type AppProps = NextAppProps & {
|
||||||
/** Will be defined only is there was an error */
|
/** Will be defined only is there was an error */
|
||||||
err?: Error;
|
err?: Error;
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
import Document, { Head, Html, Main, NextScript } from "next/document";
|
import Document, { DocumentContext, Head, Html, Main, NextScript, DocumentProps } from "next/document";
|
||||||
|
|
||||||
class MyDocument extends Document {
|
type Props = Record<string, unknown> & DocumentProps;
|
||||||
static async getInitialProps(ctx) {
|
|
||||||
|
class MyDocument extends Document<Props> {
|
||||||
|
static async getInitialProps(ctx: DocumentContext) {
|
||||||
const initialProps = await Document.getInitialProps(ctx);
|
const initialProps = await Document.getInitialProps(ctx);
|
||||||
return { ...initialProps };
|
return { ...initialProps };
|
||||||
}
|
}
|
||||||
|
|
112
pages/_error.tsx
Normal file
112
pages/_error.tsx
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
/**
|
||||||
|
* Typescript class based component for custom-error
|
||||||
|
* @link https://nextjs.org/docs/advanced-features/custom-error-page
|
||||||
|
*/
|
||||||
|
import React from "react";
|
||||||
|
import { NextPage, NextPageContext } from "next";
|
||||||
|
import NextError, { ErrorProps } from "next/error";
|
||||||
|
import { HttpError } from "@lib/core/http/error";
|
||||||
|
import { ErrorPage } from "@components/error/error-page";
|
||||||
|
import logger from "@lib/logger";
|
||||||
|
|
||||||
|
// 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.getChildLogger({ prefix: ["[error]"] });
|
||||||
|
|
||||||
|
export function getErrorFromUnknown(cause: unknown): Error {
|
||||||
|
if (cause instanceof Error) {
|
||||||
|
return cause;
|
||||||
|
}
|
||||||
|
if (typeof cause === "string") {
|
||||||
|
// @ts-expect-error https://github.com/tc39/proposal-error-cause
|
||||||
|
return new Error(cause, { cause });
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Error(`Unhandled error of type '${typeof cause}''`);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 ({ res, err, asPath }: AugmentedNextPageContext) => {
|
||||||
|
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) {
|
||||||
|
errorInitialProps.statusCode = err.statusCode;
|
||||||
|
errorInitialProps.title = err.name;
|
||||||
|
errorInitialProps.message = err.message;
|
||||||
|
errorInitialProps.err = err;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
|
@ -10,6 +10,7 @@ import { getSession } from "@lib/auth";
|
||||||
import { Scheduler } from "@components/ui/Scheduler";
|
import { Scheduler } from "@components/ui/Scheduler";
|
||||||
import { Disclosure, RadioGroup } from "@headlessui/react";
|
import { Disclosure, RadioGroup } from "@headlessui/react";
|
||||||
import { PhoneIcon, XIcon } from "@heroicons/react/outline";
|
import { PhoneIcon, XIcon } from "@heroicons/react/outline";
|
||||||
|
import { HttpError } from "@lib/core/http/error";
|
||||||
import {
|
import {
|
||||||
LocationMarkerIcon,
|
LocationMarkerIcon,
|
||||||
LinkIcon,
|
LinkIcon,
|
||||||
|
@ -82,8 +83,9 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
await router.push("/event-types");
|
await router.push("/event-types");
|
||||||
showToast(`${eventType.title} event type updated successfully`, "success");
|
showToast(`${eventType.title} event type updated successfully`, "success");
|
||||||
},
|
},
|
||||||
onError: (err: Error) => {
|
onError: (err: HttpError) => {
|
||||||
showToast(err.message, "error");
|
const message = `${err.statusCode}: ${err.message}`;
|
||||||
|
showToast(message, "error");
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -92,8 +94,9 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
await router.push("/event-types");
|
await router.push("/event-types");
|
||||||
showToast("Event type deleted successfully", "success");
|
showToast("Event type deleted successfully", "success");
|
||||||
},
|
},
|
||||||
onError: (err: Error) => {
|
onError: (err: HttpError) => {
|
||||||
showToast(err.message, "error");
|
const message = `${err.statusCode}: ${err.message}`;
|
||||||
|
showToast(message, "error");
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -29,6 +29,7 @@ import { ONBOARDING_INTRODUCED_AT } from "@lib/getting-started";
|
||||||
import { useToggleQuery } from "@lib/hooks/useToggleQuery";
|
import { useToggleQuery } from "@lib/hooks/useToggleQuery";
|
||||||
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||||
import { Alert } from "@components/ui/Alert";
|
import { Alert } from "@components/ui/Alert";
|
||||||
|
import { HttpError } from "@lib/core/http/error";
|
||||||
|
|
||||||
const EventTypesPage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
const EventTypesPage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
const { user, types } = props;
|
const { user, types } = props;
|
||||||
|
@ -39,8 +40,9 @@ const EventTypesPage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
await router.push("/event-types/" + eventType.slug);
|
await router.push("/event-types/" + eventType.slug);
|
||||||
showToast(`${eventType.title} event type created successfully`, "success");
|
showToast(`${eventType.title} event type created successfully`, "success");
|
||||||
},
|
},
|
||||||
onError: (err: Error) => {
|
onError: (err: HttpError) => {
|
||||||
showToast(err.message, "error");
|
const message = `${err.statusCode}: ${err.message}`;
|
||||||
|
showToast(message, "error");
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const modalOpen = useToggleQuery("new");
|
const modalOpen = useToggleQuery("new");
|
||||||
|
|
19
pages/sandbox/preview-error-page.tsx
Normal file
19
pages/sandbox/preview-error-page.tsx
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import { NextPage } from "next";
|
||||||
|
import { ErrorPage } from "@components/error/error-page";
|
||||||
|
import React from "react";
|
||||||
|
import { HttpError } from "@lib/core/http/error";
|
||||||
|
|
||||||
|
const PreviewErrorPage: NextPage = () => {
|
||||||
|
const statusCode = 403;
|
||||||
|
const message = `this was an http error ${statusCode}`;
|
||||||
|
const previousError = new Error("A test error");
|
||||||
|
const error = new HttpError({
|
||||||
|
statusCode,
|
||||||
|
message,
|
||||||
|
url: "http://some.invalid.url",
|
||||||
|
cause: previousError,
|
||||||
|
});
|
||||||
|
return <ErrorPage displayDebug={true} statusCode={statusCode} error={error} message={message} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PreviewErrorPage;
|
25
pages/sandbox/test-async-error.tsx
Normal file
25
pages/sandbox/test-async-error.tsx
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import React from "react";
|
||||||
|
import { HttpError } from "@lib/core/http/error";
|
||||||
|
import { useQuery } from "react-query";
|
||||||
|
|
||||||
|
const TestAsyncErrorRoute: React.FC = () => {
|
||||||
|
const { error, isLoading } = useQuery(["error-promise"], async () => {
|
||||||
|
throw new HttpError({
|
||||||
|
statusCode: 400,
|
||||||
|
message: "A http error occurred on the client side in test-async-error.tsx.",
|
||||||
|
url: "http://awebsite.that.does.not.exist",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <>Loading...</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.log("An error occurred", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
return <>If you see this message, there is really something wrong ;)</>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TestAsyncErrorRoute;
|
28
pages/sandbox/test-error.tsx
Normal file
28
pages/sandbox/test-error.tsx
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import React from "react";
|
||||||
|
import { HttpError } from "@lib/core/http/error";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
hasRunOnServer: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const TestErrorRoute: React.FC<Props> = (props) => {
|
||||||
|
if (!props.hasRunOnServer) {
|
||||||
|
throw new HttpError({ statusCode: 400, message: "test-error.tsx" });
|
||||||
|
}
|
||||||
|
return <>If you see this message, there is really something wrong ;)</>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Having a page that always throws error is very hard with nextjs
|
||||||
|
// because it will try to pre-render the page at build-time... and
|
||||||
|
// complain: 'you need to fix this'. So here because we want to always
|
||||||
|
// throw an error for monitoring, let's force server side generation
|
||||||
|
// all the time (the page won't be pre-generated, all cool).
|
||||||
|
export async function getServerSideProps() {
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
hasRunOnServer: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TestErrorRoute;
|
|
@ -1,11 +1,19 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es5",
|
"target": "es5",
|
||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"dom.iterable",
|
||||||
|
"esnext"
|
||||||
|
],
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@components/*": ["components/*"],
|
"@components/*": [
|
||||||
"@lib/*": ["lib/*"]
|
"components/*"
|
||||||
|
],
|
||||||
|
"@lib/*": [
|
||||||
|
"lib/*"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
|
@ -19,6 +27,13 @@
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"jsx": "preserve"
|
"jsx": "preserve"
|
||||||
},
|
},
|
||||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "lib/*.js"],
|
"include": [
|
||||||
"exclude": ["node_modules"]
|
"next-env.d.ts",
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
"lib/*.js"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue