From 282ef9e6734cf80f6a726345b1781054269b338b Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Tue, 6 May 2025 20:43:32 -0400 Subject: [PATCH] split exception logging from error logging to simplify and improve mastodon errors --- .../api/mastodon/MastodonApiServerService.ts | 10 ++- .../src/server/api/mastodon/MastodonLogger.ts | 81 ++++++++----------- 2 files changed, 42 insertions(+), 49 deletions(-) diff --git a/packages/backend/src/server/api/mastodon/MastodonApiServerService.ts b/packages/backend/src/server/api/mastodon/MastodonApiServerService.ts index d95d75f12f..1b0ed2ce4e 100644 --- a/packages/backend/src/server/api/mastodon/MastodonApiServerService.ts +++ b/packages/backend/src/server/api/mastodon/MastodonApiServerService.ts @@ -5,7 +5,7 @@ import { Injectable } from '@nestjs/common'; import { bindThis } from '@/decorators.js'; -import { getErrorData, getErrorStatus, MastodonLogger } from '@/server/api/mastodon/MastodonLogger.js'; +import { getErrorData, getErrorException, getErrorStatus, MastodonLogger } from '@/server/api/mastodon/MastodonLogger.js'; import { MastodonClientService } from '@/server/api/mastodon/MastodonClientService.js'; import { ApiAccountMastodon } from '@/server/api/mastodon/endpoints/account.js'; import { ApiAppsMastodon } from '@/server/api/mastodon/endpoints/apps.js'; @@ -47,16 +47,20 @@ export class MastodonApiServerService { this.serverUtilityService.addFlattenedQueryType(fastify); // Convert JS exceptions into error responses - fastify.setErrorHandler((error, _, reply) => { + fastify.setErrorHandler((error, request, reply) => { const data = getErrorData(error); const status = getErrorStatus(error); + const exception = getErrorException(error); + if (exception) { + this.logger.exception(request, exception); + } reply.code(status).send(data); }); // Log error responses (including converted JSON exceptions) fastify.addHook('onSend', (request, reply, payload, done) => { - if (reply.statusCode >= 500 || (reply.statusCode >= 400 && this.logger.verbose)) { + if (reply.statusCode >= 400 && reply.statusCode <= 500) { if (typeof(payload) === 'string' && String(reply.getHeader('content-type')).toLowerCase().includes('application/json')) { const body = JSON.parse(payload); const data = getErrorData(body); diff --git a/packages/backend/src/server/api/mastodon/MastodonLogger.ts b/packages/backend/src/server/api/mastodon/MastodonLogger.ts index 8d8f62bc0d..29d44918a3 100644 --- a/packages/backend/src/server/api/mastodon/MastodonLogger.ts +++ b/packages/backend/src/server/api/mastodon/MastodonLogger.ts @@ -25,13 +25,29 @@ export class MastodonLogger { } public error(request: FastifyRequest, error: MastodonError, status: number): void { - const path = new URL(request.url, getBaseUrl(request)).pathname; + const path = getPath(request); + if (status >= 400 && status <= 499) { // Client errors this.logger.debug(`Error in mastodon endpoint ${request.method} ${path}:`, error); } else { // Server errors this.logger.error(`Error in mastodon endpoint ${request.method} ${path}:`, error); } } + + public exception(request: FastifyRequest, ex: Error): void { + const path = getPath(request); + + // Exceptions are always server errors, and should therefore always be logged. + this.logger.error(`Error in mastodon endpoint ${request.method} ${path}:`, ex); + } +} + +function getPath(request: FastifyRequest): string { + try { + return new URL(request.url, getBaseUrl(request)).pathname; + } catch { + return request.url; + } } // TODO move elsewhere @@ -40,6 +56,17 @@ export interface MastodonError { error_description?: string; } +export function getErrorException(error: unknown): Error | null { + if (error instanceof Error) { + // Axios errors are not exceptions - they're from the remote + if (!('response' in error) || !error.response || typeof (error.response) !== 'object') { + return error; + } + } + + return null; +} + export function getErrorData(error: unknown): MastodonError { // Axios wraps errors from the backend error = unpackAxiosError(error); @@ -78,7 +105,10 @@ export function getErrorData(error: unknown): MastodonError { } } - return convertUnknownError(error); + return { + error: 'INTERNAL_ERROR', + error_description: 'Internal error occurred. Please contact us if the error persists.', + }; } function unpackAxiosError(error: unknown): unknown { @@ -101,66 +131,25 @@ function unpackAxiosError(error: unknown): unknown { } function convertApiError(apiError: ApiError): MastodonError { - const mastoError: MastodonError & Partial & { stack?: unknown, statusCode?: number } = { - ...apiError, + return { error: apiError.code, error_description: apiError.message, }; - - delete mastoError.code; - delete mastoError.stack; - delete mastoError.message; - delete mastoError.httpStatusCode; - - return mastoError; } function convertErrorMessageError(error: { error: string, message: string }): MastodonError { - const mastoError: MastodonError & { stack?: unknown, message?: string, statusCode?: number } = { - ...error, + return { error: error.error, error_description: error.message, }; - - delete mastoError.stack; - delete mastoError.message; - delete mastoError.statusCode; - - return mastoError; -} - -function convertUnknownError(data: object = {}): MastodonError { - const mastoError = Object.assign({}, data, { - error: 'INTERNAL_ERROR', - error_description: 'Internal error occurred. Please contact us if the error persists.', - id: '5d37dbcb-891e-41ca-a3d6-e690c97775ac', - kind: 'server', - }); - - if ('statusCode' in mastoError) { - delete mastoError.statusCode; - } - - if ('stack' in mastoError) { - delete mastoError.stack; - } - - return mastoError; } function convertGenericError(error: Error): MastodonError { - const mastoError: MastodonError & Partial & { statusCode?: number } = { + return { ...error, error: 'INTERNAL_ERROR', error_description: String(error), }; - - delete mastoError.name; - delete mastoError.message; - delete mastoError.stack; - delete mastoError.statusCode; - - return mastoError; } export function getErrorStatus(error: unknown): number {