split exception logging from error logging to simplify and improve mastodon errors

This commit is contained in:
Hazelnoot 2025-05-06 20:43:32 -04:00
parent f7ca7a2cc0
commit 282ef9e673
2 changed files with 42 additions and 49 deletions

View file

@ -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);

View file

@ -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<ApiError> & { 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<Error> & { 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 {