fix(error,view): make the InternalErrorView (+ add 404) more useful@@ -1,5 +1,5 @@
{
- "_generatedAtUnix": 1665337654768,
+ "_generatedAtUnix": 1665547078652,
"_hashAlgorithm": "sha1",
"_version": 2,
"islands": {
@@ -82,7 +82,7 @@
"pathSource": "./app/views/HomeView.tsx"
},
"InternalErrorView": {
- "hash": "236319fa6d5bd4b376ba18bd9cb166b5fc34fc55",
+ "hash": "63c8e65201adc83014dd9434569fb857d3888635",
"pathSource": "./app/views/InternalErrorView.tsx"
},
"LoginView": {
@@ -21,6 +21,10 @@ import { GlobalRole, PrismaClient } from "@prisma/client";
// app root
import * as Paths from "../paths";
import { version as appVersion } from "../package.json";
+// app views
+import InternalErrorView, {
+ InternalErrorViewProps,
+} from "./views/InternalErrorView";
// app
import { Const } from "./const";
import { Env } from "./env";
@@ -31,6 +35,7 @@ import {
localAppDomainPreHandler,
makeRequestHandler,
} from "./utils/server";
+import { FastifyError } from "fastify";
let server: null | AppServer = null;
@@ -70,6 +75,7 @@ async function main(): Promise<AppServer> {
withInstantRouter: true,
withStyledSSR: true,
withTreeShaking: true,
+ withDefaultErrorHandlers: false,
},
paths: {
assetsOutFolder: Paths.PUBLIC_FOLDER,
@@ -194,19 +200,47 @@ async function main(): Promise<AppServer> {
// add a preHandler to warn against bad localhost usage (so cookies works)
s.addHook("preHandler", localAppDomainPreHandler);
+ // ensure strings do not leak server path.
+ const cleanupFastifyError = (error: Error | FastifyError) => {
+ error.message = error.message.replaceAll(Paths.ROOT_FOLDER, "");
+ error.stack = error.stack?.replaceAll(Paths.ROOT_FOLDER, "");
+ };
+
// load Prism languages/grammars to enable SSR syntax highlighting
try {
loadPrismJsLanguages();
} catch (err) {
+ const error = err as Error;
+ cleanupFastifyError(error);
console.error(
- "Could not load Prism.JS languages, syntax highlighting may not work as expected.",
- `Error: ${(err as Error).message}`
+ "Couldn't load Prism.JS languages, syntax highlighting will not work properly.",
+ `Error: ${error.message}`
);
}
// add a reply decorator so we can reply with common props from app
s.decorateReply("makeRequestHandler", makeRequestHandler);
+ // set the error handler so it renders to InternalErrorView
+ s.setErrorHandler((error, request, reply) => {
+ cleanupFastifyError(error);
+ const reqHandler = reply.makeRequestHandler(request, reply);
+ return reqHandler<InternalErrorViewProps>(InternalErrorView.name, {
+ error,
+ });
+ });
+
+ // set the not found handler so it renders to InternalErrorView with 404 code
+ s.setNotFoundHandler((request, reply) => {
+ const error = new Error("404 - Not Found");
+ error.name = "NotFoundError";
+ cleanupFastifyError(error);
+ const reqHandler = reply.makeRequestHandler(request, reply);
+ return reqHandler<InternalErrorViewProps>(InternalErrorView.name, {
+ error,
+ });
+ });
+
// register the code analysis plugin
s.register(codeAnalysisPlugin);
@@ -5,12 +5,15 @@ import React from "react";
// app
import type { CommonProps } from "../types";
-import { Layout } from "../components";
+import { Card, Layout, PageWrapper } from "../components";
+// app islands
+import Code, { getThemedCodeCss } from "../islands/Code";
export interface InternalErrorViewProps extends CommonProps {
error: FastifyError;
}
+const DEV = process.env.NODE_ENV === "development";
const DEBUG = !!(
process.env.DEBUG != null && ["true", "1", true].includes(process.env.DEBUG)
);
@@ -19,29 +22,112 @@ const InternalErrorView: ReactView<InternalErrorViewProps> = ({
commonProps,
error,
}) => {
+ let { code } = error;
+ const { message, stack, validation } = error;
+ const statusCode = Array.isArray(message.match(/404 - Not Found/i))
+ ? 404
+ : error.statusCode || 500;
+
+ if (code == null || code.trim() === "") {
+ code = statusCode.toString();
+ }
+
+ const isNotFoundError = statusCode === 404;
+ const isRequestError =
+ !isNotFoundError && statusCode >= 400 && statusCode <= 499;
+ const isInternalError = statusCode >= 500;
+
+ const isRecoverableError =
+ isRequestError && !isNotFoundError && !isInternalError;
+
return (
<Layout {...commonProps}>
- <h1>⚡️😵💫 Woops... we've encountered an internal error.</h1>
- <p>Sorry but we cannot recover from this error...</p>
- {(DEBUG || process.env.NODE_ENV === "development") && (
- <details>
- <summary>Find out more about this error (expert mode):</summary>
- <p>
- [{error.code}] {error.name}: {error.name}
- </p>
- {error.stack != null && <p>{error.stack}</p>}
- {error.validation != null && (
- <p>{JSON.stringify(error.validation, null, 2)}</p>
- )}
- </details>
- )}
- <a
- href="/"
- title={"Hit that bug super hard, that may work!"}
- role={"button"}
- >
- 🐞 Try again
- </a>
+ <PageWrapper>
+ {isRequestError && (
+ <h1>🤔 Mh, something was not correct with your request</h1>
+ )}
+ {isNotFoundError && (
+ <h1>
+ 🔭 Looks like this page is missing, or has never been there...
+ </h1>
+ )}
+ {isInternalError && (
+ <h1>
+ 😵💫 Woops... we've encountered an internal error, please
+ apologize.
+ </h1>
+ )}
+ <div style={{ marginTop: 8 }}>
+ {!isNotFoundError &&
+ (isRecoverableError ? (
+ <a href="/" role={"button"}>
+ Try again 🔄
+ </a>
+ ) : (
+ <p>Sorry but it is not possible to recover from this error.</p>
+ ))}
+ </div>
+ {(DEBUG || DEV) && (
+ <div style={{ marginTop: 24 }}>
+ {getThemedCodeCss(commonProps.themeScheme)}
+ <details open>
+ <summary>[DEBUG] Full error details:</summary>
+ {message != null && message.trim() !== "" && (
+ <div style={{ marginTop: 16 }}>
+ <label
+ style={{ fontWeight: "bold", textDecoration: "underline" }}
+ >
+ Message:
+ </label>
+ <pre>
+ <code>{message.trim()}</code>
+ </pre>
+ </div>
+ )}
+ {stack != null && stack.trim() !== "" && (
+ <div style={{ marginTop: 16 }}>
+ <label
+ style={{ fontWeight: "bold", textDecoration: "underline" }}
+ >
+ Stack:
+ </label>
+ <Card
+ data-islandid={`${Code.name}$$0`}
+ style={{ width: "100%", marginTop: 32 }}
+ themeScheme={commonProps.themeScheme}
+ >
+ <Code
+ language={"python"}
+ code={stack.replace(message, "").trim()}
+ themeScheme={commonProps.themeScheme}
+ />
+ </Card>
+ </div>
+ )}
+ {validation != null && (
+ <div style={{ marginTop: 16 }}>
+ <label
+ style={{ fontWeight: "bold", textDecoration: "underline" }}
+ >
+ Validation:
+ </label>
+ <Card
+ data-islandid={`${Code.name}$$1`}
+ style={{ width: "100%", marginTop: 32 }}
+ themeScheme={commonProps.themeScheme}
+ >
+ <Code
+ language={"json"}
+ code={JSON.stringify(validation, null, 2)}
+ themeScheme={commonProps.themeScheme}
+ />
+ </Card>
+ </div>
+ )}
+ </details>
+ </div>
+ )}
+ </PageWrapper>
</Layout>
);
};
@@ -3,7 +3,7 @@
"baseUrl": ".",
"declaration": true,
"incremental": false,
- "lib": ["es2020", "dom"],
+ "lib": ["es2021", "dom"],
"jsx": "react",
"module": "commonjs",
"moduleResolution": "node",