Add formatted error pages for console and browser

Errors now show app frames highlighted with relative paths and library
frames collapsed, both in ANSI on the console and as a styled HTML page
in the browser. Process-level uncaughtException/unhandledRejection
handlers also use the formatter.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-07 18:07:41 -05:00
parent a39ed37a03
commit 91780b6dca
4 changed files with 437 additions and 157 deletions

View File

@@ -1,171 +1,30 @@
import express, {
type Request as ExpressRequest,
type Response as ExpressResponse,
} from "express";
import { match } from "path-to-regexp";
import { Session } from "./diachron/auth";
import { cli } from "./diachron/cli";
import { contentTypes } from "./diachron/content-types";
import { runWithContext } from "./diachron/context";
import{cli}from'./diachron/cli'
import { formatError } from './diachron/errors';
process.on('uncaughtException', (err) => {
console.error(formatError(err));
process.exit(1);
});
process.on('unhandledRejection', (reason) => {
console.error(formatError(reason));
});
import { core } from "./diachron/core";
import { httpCodes } from "./diachron/http-codes";
import { request } from "./diachron/request";
// import { URLPattern } from 'node:url';
import {
AuthenticationRequired,
AuthorizationDenied,
type Call,
type InternalHandler,
isRedirect,
type Method,
massageMethod,
methodParser,
type ProcessedRoute,
type Result,
type Route,
} from "./diachron/types";
import { routes } from "./routes";
import {makeApp}from'./diachron/app'
const app = express();
// Parse request bodies
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
const app = makeApp({routes});
core.logging.log({ source: "logging", text: ["1"] });
const processedRoutes: { [K in Method]: ProcessedRoute[] } = {
GET: [],
POST: [],
PUT: [],
PATCH: [],
DELETE: [],
};
function _isPromise<T>(value: T | Promise<T>): value is Promise<T> {
return typeof (value as any)?.then === "function";
}
routes.forEach((route: Route, _idx: number, _allRoutes: Route[]) => {
// const pattern /*: URLPattern */ = new URLPattern({ pathname: route.path });
const matcher = match<Record<string, string>>(route.path);
const methodList = route.methods;
const handler: InternalHandler = async (
expressRequest: ExpressRequest,
): Promise<Result> => {
const method = massageMethod(expressRequest.method);
console.log("method", method);
if (!methodList.includes(method)) {
// XXX: Worth asserting this?
}
console.log("request.originalUrl", expressRequest.originalUrl);
// Authenticate the request
const auth = await request.auth.validateRequest(expressRequest);
const req: Call = {
pattern: route.path,
path: expressRequest.originalUrl,
method,
parameters: { one: 1, two: 2 },
request: expressRequest,
user: auth.user,
session: new Session(auth.session, auth.user),
};
try {
const retval = await runWithContext({ user: auth.user }, () =>
route.handler(req),
);
return retval;
} catch (error) {
// Handle authentication errors
if (error instanceof AuthenticationRequired) {
return {
code: httpCodes.clientErrors.Unauthorized,
contentType: contentTypes.application.json,
result: JSON.stringify({
error: "Authentication required",
}),
};
}
if (error instanceof AuthorizationDenied) {
return {
code: httpCodes.clientErrors.Forbidden,
contentType: contentTypes.application.json,
result: JSON.stringify({ error: "Access denied" }),
};
}
throw error;
}
};
for (const [_idx, method] of methodList.entries()) {
const pr: ProcessedRoute = { matcher, method, handler };
processedRoutes[method].push(pr);
}
});
async function handler(
req: ExpressRequest,
_res: ExpressResponse,
): Promise<Result> {
const method = await methodParser.parseAsync(req.method);
const byMethod = processedRoutes[method];
console.log(
"DEBUG: req.path =",
JSON.stringify(req.path),
"method =",
method,
);
for (const [_idx, pr] of byMethod.entries()) {
const match = pr.matcher(req.path);
console.log("DEBUG: trying pattern, match result =", match);
if (match) {
console.log("match", match);
const resp = await pr.handler(req);
return resp;
}
}
const retval: Result = {
code: httpCodes.clientErrors.NotFound,
contentType: contentTypes.text.plain,
result: "not found!",
};
return retval;
}
app.use(async (req: ExpressRequest, res: ExpressResponse) => {
const result0 = await handler(req, res);
const code = result0.code.code;
const result = result0.result;
console.log(result);
// Set any cookies from the result
if (result0.cookies) {
for (const cookie of result0.cookies) {
res.cookie(cookie.name, cookie.value, cookie.options ?? {});
}
}
if (isRedirect(result0)) {
res.redirect(code, result0.redirect);
} else {
res.status(code).send(result);
}
});
process.title = `diachron:${cli.listen.port}`;
app.listen(cli.listen.port, cli.listen.host, () => {
console.log(`Listening on ${cli.listen.host}:${cli.listen.port}`);