Fix logged errors not being properly formatted (and tone down the error verbosity a bit) (#874)

* Ensure we add the error handlers last

* Refactor error middleware to be less noisy

* changelog

* Add finalise
This commit is contained in:
Will Hunt 2024-01-02 15:58:25 +00:00 committed by GitHub
parent c0bb71d553
commit 41ee467e2d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 24 additions and 13 deletions

2
changelog.d/874.bugfix Normal file
View File

@ -0,0 +1,2 @@
Fix hookshot failing to format API errors.
Only log a stacktrace of API errors on debug level logging, log limited error on info.

View File

@ -233,6 +233,7 @@ export class E2ETestEnv {
} }
}; };
const app = await start(config, registration); const app = await start(config, registration);
app.listener.finaliseListeners();
return new E2ETestEnv(homeserver, app, opts, config, dir); return new E2ETestEnv(homeserver, app, opts, config, dir);
} }

View File

@ -73,8 +73,9 @@ async function startFromFile() {
const registrationFile = process.argv[3] || "./registration.yml"; const registrationFile = process.argv[3] || "./registration.yml";
const config = await BridgeConfig.parseConfig(configFile, process.env); const config = await BridgeConfig.parseConfig(configFile, process.env);
const registration = await parseRegistrationFile(registrationFile); const registration = await parseRegistrationFile(registrationFile);
const { bridgeApp } = await start(config, registration); const { bridgeApp, listener } = await start(config, registration);
await bridgeApp.start(); await bridgeApp.start();
listener.finaliseListeners();
} }
if (require.main === module) { if (require.main === module) {

View File

@ -30,6 +30,7 @@ async function start() {
} }
const webhookHandler = new Webhooks(config); const webhookHandler = new Webhooks(config);
listener.bindResource('webhooks', webhookHandler.expressRouter); listener.bindResource('webhooks', webhookHandler.expressRouter);
listener.finaliseListeners();
const userWatcher = new UserNotificationWatcher(config); const userWatcher = new UserNotificationWatcher(config);
userWatcher.start(); userWatcher.start();
process.once("SIGTERM", () => { process.once("SIGTERM", () => {

View File

@ -32,6 +32,7 @@ async function start() {
listener.bindResource('metrics', Metrics.expressRouter); listener.bindResource('metrics', Metrics.expressRouter);
} }
} }
listener.finaliseListeners();
sender.listen(); sender.listen();
process.once("SIGTERM", () => { process.once("SIGTERM", () => {
log.error("Got SIGTERM"); log.error("Got SIGTERM");

View File

@ -51,6 +51,14 @@ export class ListenerService {
} }
} }
public finaliseListeners() {
for (const listener of this.listeners) {
// By default, Sentry only reports 500+ errors, which is what we want.
listener.app.use(Handlers.errorHandler());
listener.app.use((err: unknown, req: Request, res: Response, next: NextFunction) => errorMiddleware(log)(err, req, res, next));
}
}
public getApplicationsForResource(resourceName: ResourceName): Application[] { public getApplicationsForResource(resourceName: ResourceName): Application[] {
const listeners = this.listeners.filter((l) => l.config.resources.includes(resourceName)); const listeners = this.listeners.filter((l) => l.config.resources.includes(resourceName));
if (listeners.length === 0) { if (listeners.length === 0) {
@ -74,11 +82,6 @@ export class ListenerService {
// Ensure each listener has a ready probe. // Ensure each listener has a ready probe.
listener.app.get("/live", (_, res) => res.send({ok: true})); listener.app.get("/live", (_, res) => res.send({ok: true}));
listener.app.get("/ready", (_, res) => res.status(listener.resourcesBound ? 200 : 500).send({ready: listener.resourcesBound})); listener.app.get("/ready", (_, res) => res.status(listener.resourcesBound ? 200 : 500).send({ready: listener.resourcesBound}));
// By default, Sentry only reports 500+ errors, which is what we want.
listener.app.use(Handlers.errorHandler());
// Always include the error handler
listener.app.use((err: unknown, req: Request, res: Response, next: NextFunction) => errorMiddleware(log)(err, req, res, next));
log.info(`Listening on http://${addr}:${listener.config.port} for ${listener.config.resources.join(', ')}`) log.info(`Listening on http://${addr}:${listener.config.port} for ${listener.config.resources.join(', ')}`)
} }
} }

View File

@ -70,6 +70,8 @@ const ErrCodeToStatusCode: Record<ErrCode, StatusCodes> = {
} }
export class ApiError extends Error implements IApiError { export class ApiError extends Error implements IApiError {
static readonly GenericFailure = new ApiError("An internal error occurred");
constructor( constructor(
public readonly error: string, public readonly error: string,
public readonly errcode = ErrCode.Unknown, public readonly errcode = ErrCode.Unknown,
@ -109,19 +111,19 @@ export class ValidatorApiError extends ApiError {
export function errorMiddleware(log: Logger) { export function errorMiddleware(log: Logger) {
return (err: unknown, _req: Request, res: Response, next: NextFunction) => { return (err: unknown, req: Request, res: Response, next: NextFunction) => {
if (!err) { if (!err) {
next(); next();
return; return;
} }
log.warn(err); const apiError = err instanceof ApiError ? err : ApiError.GenericFailure;
// Log a reduced error on info
log.info(`${req.method} ${req.path} ${apiError.statusCode} - ${apiError.errcode} - ${apiError.error}`);
// Only show full error on debug level.
log.debug(err);
if (res.headersSent) { if (res.headersSent) {
return; return;
} }
if (err instanceof ApiError) { apiError.apply(res);
err.apply(res);
} else {
new ApiError("An internal error occurred").apply(res);
}
} }
} }