fix(error,view): make the InternalErrorView (+ add 404) more useful
+ 147
- 27
@@ -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);
 

app/views/InternalErrorView.tsx
@@ -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",

GitFOSS - v0.2.0 (#48b426e) - MIT License