1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
|
/*
* SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Injectable } from '@nestjs/common';
import { isAxiosError } from 'axios';
import type Logger from '@/logger.js';
import { LoggerService } from '@/core/LoggerService.js';
import { ApiError } from '@/server/api/error.js';
import { getBaseUrl } from '@/server/api/mastodon/MastodonClientService.js';
import { AuthenticationError } from '@/server/api/AuthenticateService.js';
import type { FastifyRequest } from 'fastify';
@Injectable()
export class MastodonLogger {
public readonly logger: Logger;
constructor(
loggerService: LoggerService,
) {
this.logger = loggerService.getLogger('masto-api');
}
public error(request: FastifyRequest, error: MastodonError, status: number): void {
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(`Exception 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
export interface MastodonError {
error: string;
error_description?: string;
}
export function getErrorException(error: unknown): Error | null {
if (!(error instanceof Error)) {
return null;
}
// AxiosErrors need special decoding
if (isAxiosError(error)) {
// Axios errors with a response are from the remote
if (error.response) {
return null;
}
// This is the inner exception, basically
if (error.cause && !isAxiosError(error.cause)) {
if (!error.cause.stack) {
error.cause.stack = error.stack;
}
return error.cause;
}
const ex = new Error();
ex.name = error.name;
ex.stack = error.stack;
ex.message = error.message;
ex.cause = error.cause;
return ex;
}
// AuthenticationError is a client error
if (error instanceof AuthenticationError) {
return null;
}
return error;
}
export function getErrorData(error: unknown): MastodonError {
// Axios wraps errors from the backend
error = unpackAxiosError(error);
if (!error || typeof(error) !== 'object') {
return {
error: 'UNKNOWN_ERROR',
error_description: String(error),
};
}
if (error instanceof ApiError) {
return convertApiError(error);
}
if ('code' in error && typeof (error.code) === 'string') {
if ('message' in error && typeof (error.message) === 'string') {
return convertApiError(error as ApiError);
}
}
if ('error' in error && typeof (error.error) === 'string') {
if ('message' in error && typeof (error.message) === 'string') {
return convertErrorMessageError(error as { error: string, message: string });
}
}
if (error instanceof Error) {
return convertGenericError(error);
}
if ('error' in error && typeof(error.error) === 'string') {
// "error_description" is string, undefined, or not present.
if (!('error_description' in error) || typeof(error.error_description) === 'string' || typeof(error.error_description) === 'undefined') {
return convertMastodonError(error as MastodonError);
}
}
return {
error: 'INTERNAL_ERROR',
error_description: 'Internal error occurred. Please contact us if the error persists.',
};
}
function unpackAxiosError(error: unknown): unknown {
if (isAxiosError(error)) {
if (error.response) {
if (error.response.data && typeof(error.response.data) === 'object') {
if ('error' in error.response.data && error.response.data.error && typeof(error.response.data.error) === 'object') {
return error.response.data.error;
}
return error.response.data;
}
// No data - this is a fallback to avoid leaking request/response details in the error
return undefined;
}
if (error.cause && !isAxiosError(error.cause)) {
if (!error.cause.stack) {
error.cause.stack = error.stack;
}
return error.cause;
}
// No data - this is a fallback to avoid leaking request/response details in the error
return String(error);
}
return error;
}
function convertApiError(apiError: ApiError): MastodonError {
return {
error: apiError.code,
error_description: apiError.message,
};
}
function convertErrorMessageError(error: { error: string, message: string }): MastodonError {
return {
error: error.error,
error_description: error.message,
};
}
function convertGenericError(error: Error): MastodonError {
return {
error: 'INTERNAL_ERROR',
error_description: String(error),
};
}
function convertMastodonError(error: MastodonError): MastodonError {
return {
error: error.error,
error_description: error.error_description,
};
}
export function getErrorStatus(error: unknown): number {
if (error && typeof(error) === 'object') {
// Axios wraps errors from the backend
if ('response' in error && typeof (error.response) === 'object' && error.response) {
if ('status' in error.response && typeof(error.response.status) === 'number') {
return error.response.status;
}
}
if ('httpStatusCode' in error && typeof(error.httpStatusCode) === 'number') {
return error.httpStatusCode;
}
if ('statusCode' in error && typeof(error.statusCode) === 'number') {
return error.statusCode;
}
}
return 500;
}
|