@pori15/logixlysia 6.0.1 → 6.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +36 -10
- package/dist/index.js +7 -7
- package/package.json +2 -2
- package/src/Error/errors.ts +258 -258
- package/src/Error/type.ts +51 -51
- package/src/extensions/banner.ts +26 -26
- package/src/extensions/index.ts +28 -28
- package/src/helpers/status.ts +58 -58
- package/src/index.ts +162 -141
- package/src/interfaces.ts +138 -145
- package/src/logger/create-logger.ts +240 -246
- package/src/logger/handle-http-error.ts +60 -63
- package/src/logger/index.ts +138 -139
- package/src/output/file.ts +85 -85
- package/src/output/fs.ts +5 -5
- package/src/output/index.ts +58 -58
- package/src/output/rotation-manager.ts +122 -122
- package/src/utils/error.ts +13 -13
- package/src/utils/handle-error.ts +284 -289
- package/src/utils/rotation.ts +91 -91
|
@@ -1,289 +1,284 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* RFC 9457 Problem JSON 格式化工具
|
|
3
|
-
* @see https://www.rfc-editor.org/rfc/rfc9457.html
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { ProblemError } from
|
|
7
|
-
import { Code } from
|
|
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
|
-
let
|
|
46
|
-
let
|
|
47
|
-
let
|
|
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
|
-
export interface ProblemJson {
|
|
92
|
-
type?: string
|
|
93
|
-
title: string
|
|
94
|
-
status: number
|
|
95
|
-
detail?: string
|
|
96
|
-
instance?: string
|
|
97
|
-
[key: string]: unknown
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
export interface ProblemJsonOptions {
|
|
101
|
-
/**
|
|
102
|
-
* 基础 URL,用于生成错误类型的链接
|
|
103
|
-
* @example 'https://api.example.com/errors'
|
|
104
|
-
*/
|
|
105
|
-
typeBaseUrl?: string
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* 是否在日志中显示完整的 Problem JSON
|
|
109
|
-
* @default true
|
|
110
|
-
*/
|
|
111
|
-
enabled?: boolean
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* 自定义格式化函数
|
|
115
|
-
*/
|
|
116
|
-
format?: (error: unknown, request: Request) => ProblemJson | null
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const isErrorWithStatus = (
|
|
120
|
-
value
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
value
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
value
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
)
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
const
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
request
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
)
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
([key])
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
return
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
if (status >= 400) return '🟡'
|
|
286
|
-
if (status >= 300) return '🔵'
|
|
287
|
-
if (status >= 200) return '🟢'
|
|
288
|
-
return '⚪'
|
|
289
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* RFC 9457 Problem JSON 格式化工具
|
|
3
|
+
* @see https://www.rfc-editor.org/rfc/rfc9457.html
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { ProblemError } from '../Error/errors'
|
|
7
|
+
import type { Code } from '../Error/type'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 默认的状态码与标题映射表 (RFC 标准推荐)
|
|
11
|
+
*/
|
|
12
|
+
const DEFAULT_TITLES: Record<number, string> = {
|
|
13
|
+
400: 'Bad Request',
|
|
14
|
+
401: 'Unauthorized',
|
|
15
|
+
403: 'Forbidden',
|
|
16
|
+
404: 'Not Found',
|
|
17
|
+
409: 'Conflict',
|
|
18
|
+
422: 'Unprocessable Entity',
|
|
19
|
+
500: 'Internal Server Error',
|
|
20
|
+
503: 'Service Unavailable'
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const normalizeToProblem = (
|
|
24
|
+
error: any,
|
|
25
|
+
code: Code,
|
|
26
|
+
path: string,
|
|
27
|
+
typeBaseUrl = 'about:blank'
|
|
28
|
+
): ProblemError => {
|
|
29
|
+
// 1. 如果已经是 ProblemError,直接补充 instance 并返回
|
|
30
|
+
if (error instanceof ProblemError) {
|
|
31
|
+
// 如果没有 instance,自动补全为当前请求路径
|
|
32
|
+
return error.instance
|
|
33
|
+
? error
|
|
34
|
+
: new ProblemError(
|
|
35
|
+
error.type,
|
|
36
|
+
error.title,
|
|
37
|
+
error.status,
|
|
38
|
+
error.detail,
|
|
39
|
+
path,
|
|
40
|
+
error.extensions
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 2. 初始化默认值
|
|
45
|
+
let status = 500
|
|
46
|
+
let title = 'Internal Server Error'
|
|
47
|
+
let detail = error instanceof Error ? error.message : String(error)
|
|
48
|
+
let extensions: Record<string, unknown> = {}
|
|
49
|
+
|
|
50
|
+
// 3. 识别 Elysia 内置错误码并“对齐”标准
|
|
51
|
+
switch (code) {
|
|
52
|
+
case 'VALIDATION':
|
|
53
|
+
status = 400
|
|
54
|
+
title = 'Validation Failed'
|
|
55
|
+
// 提取 Elysia 的校验细节
|
|
56
|
+
extensions = { errors: error.all || [] }
|
|
57
|
+
break
|
|
58
|
+
case 'NOT_FOUND':
|
|
59
|
+
status = 404
|
|
60
|
+
title = 'Resource Not Found'
|
|
61
|
+
break
|
|
62
|
+
case 'PARSE':
|
|
63
|
+
status = 400
|
|
64
|
+
title = 'Invalid Payload'
|
|
65
|
+
detail = 'The request body could not be parsed as valid JSON.'
|
|
66
|
+
break
|
|
67
|
+
case 'INVALID_COOKIE_SIGNATURE':
|
|
68
|
+
status = 401
|
|
69
|
+
title = 'Invalid Credentials'
|
|
70
|
+
break
|
|
71
|
+
default:
|
|
72
|
+
// 如果错误对象本身带有状态码(比如某些库抛出的)
|
|
73
|
+
if (typeof error?.status === 'number') {
|
|
74
|
+
status = error.status
|
|
75
|
+
title = DEFAULT_TITLES[status] || 'Unknown Error'
|
|
76
|
+
}
|
|
77
|
+
break
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 4. 构造并返回标准的 ProblemError
|
|
81
|
+
return new ProblemError(
|
|
82
|
+
typeBaseUrl === 'about:blank' ? typeBaseUrl : `${typeBaseUrl}/${code}`,
|
|
83
|
+
title,
|
|
84
|
+
status,
|
|
85
|
+
detail,
|
|
86
|
+
path,
|
|
87
|
+
extensions
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface ProblemJson {
|
|
92
|
+
type?: string
|
|
93
|
+
title: string
|
|
94
|
+
status: number
|
|
95
|
+
detail?: string
|
|
96
|
+
instance?: string
|
|
97
|
+
[key: string]: unknown
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface ProblemJsonOptions {
|
|
101
|
+
/**
|
|
102
|
+
* 基础 URL,用于生成错误类型的链接
|
|
103
|
+
* @example 'https://api.example.com/errors'
|
|
104
|
+
*/
|
|
105
|
+
typeBaseUrl?: string
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* 是否在日志中显示完整的 Problem JSON
|
|
109
|
+
* @default true
|
|
110
|
+
*/
|
|
111
|
+
enabled?: boolean
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 自定义格式化函数
|
|
115
|
+
*/
|
|
116
|
+
format?: (error: unknown, request: Request) => ProblemJson | null
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const isErrorWithStatus = (value: unknown): value is { status: number } =>
|
|
120
|
+
typeof value === 'object' &&
|
|
121
|
+
value !== null &&
|
|
122
|
+
'status' in value &&
|
|
123
|
+
typeof (value as { status?: unknown }).status === 'number'
|
|
124
|
+
|
|
125
|
+
const isErrorLike = (value: unknown): value is Error =>
|
|
126
|
+
value instanceof Error ||
|
|
127
|
+
(typeof value === 'object' &&
|
|
128
|
+
value !== null &&
|
|
129
|
+
'message' in value &&
|
|
130
|
+
typeof (value as { message?: unknown }).message === 'string')
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* 将错误转换为 RFC 9457 Problem JSON 格式
|
|
134
|
+
*/
|
|
135
|
+
export function toProblemJson(
|
|
136
|
+
error: unknown,
|
|
137
|
+
request: Request,
|
|
138
|
+
options: ProblemJsonOptions = {}
|
|
139
|
+
): ProblemJson {
|
|
140
|
+
// 如果提供了自定义格式化函数,使用它
|
|
141
|
+
if (options.format) {
|
|
142
|
+
const custom = options.format(error, request)
|
|
143
|
+
if (custom) {
|
|
144
|
+
return custom
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const url = new URL(request.url)
|
|
149
|
+
const status = isErrorWithStatus(error) ? error.status : 500
|
|
150
|
+
const message = isErrorLike(error)
|
|
151
|
+
? error.message
|
|
152
|
+
: String(error ?? 'Unknown Error')
|
|
153
|
+
|
|
154
|
+
// 默认的 Problem JSON 结构
|
|
155
|
+
const problem: ProblemJson = {
|
|
156
|
+
type: options.typeBaseUrl
|
|
157
|
+
? `${options.typeBaseUrl}/${status}`
|
|
158
|
+
: 'about:blank',
|
|
159
|
+
title: getDefaultTitle(status),
|
|
160
|
+
status,
|
|
161
|
+
detail: message,
|
|
162
|
+
instance: url.pathname + url.search
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// 尝试从错误对象中提取额外的信息
|
|
166
|
+
if (typeof error === 'object' && error !== null) {
|
|
167
|
+
const err = error as Record<string, unknown>
|
|
168
|
+
|
|
169
|
+
// 如果错误已经有 Problem JSON 结构,直接使用
|
|
170
|
+
if ('type' in err || 'title' in err) {
|
|
171
|
+
if (err.type) problem.type = err.type as string
|
|
172
|
+
if (err.title) problem.title = err.title as string
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// 添加其他扩展字段(排除标准字段)
|
|
176
|
+
for (const [key, value] of Object.entries(err)) {
|
|
177
|
+
if (
|
|
178
|
+
!['status', 'message', 'type', 'title', 'detail', 'instance'].includes(
|
|
179
|
+
key
|
|
180
|
+
)
|
|
181
|
+
) {
|
|
182
|
+
problem[key] = value
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return problem
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* 根据状态码获取默认标题
|
|
192
|
+
*/
|
|
193
|
+
function getDefaultTitle(status: number): string {
|
|
194
|
+
const titles: Record<number, string> = {
|
|
195
|
+
400: 'Bad Request',
|
|
196
|
+
401: 'Unauthorized',
|
|
197
|
+
402: 'Payment Required',
|
|
198
|
+
403: 'Forbidden',
|
|
199
|
+
404: 'Not Found',
|
|
200
|
+
405: 'Method Not Allowed',
|
|
201
|
+
406: 'Not Acceptable',
|
|
202
|
+
407: 'Proxy Authentication Required',
|
|
203
|
+
408: 'Request Timeout',
|
|
204
|
+
409: 'Conflict',
|
|
205
|
+
410: 'Gone',
|
|
206
|
+
411: 'Length Required',
|
|
207
|
+
412: 'Precondition Failed',
|
|
208
|
+
413: 'Payload Too Large',
|
|
209
|
+
414: 'URI Too Long',
|
|
210
|
+
415: 'Unsupported Media Type',
|
|
211
|
+
416: 'Range Not Satisfiable',
|
|
212
|
+
417: 'Expectation Failed',
|
|
213
|
+
418: "I'm a teapot",
|
|
214
|
+
422: 'Unprocessable Entity',
|
|
215
|
+
423: 'Locked',
|
|
216
|
+
424: 'Failed Dependency',
|
|
217
|
+
425: 'Too Early',
|
|
218
|
+
426: 'Upgrade Required',
|
|
219
|
+
428: 'Precondition Required',
|
|
220
|
+
429: 'Too Many Requests',
|
|
221
|
+
431: 'Request Header Fields Too Large',
|
|
222
|
+
451: 'Unavailable For Legal Reasons',
|
|
223
|
+
500: 'Internal Server Error',
|
|
224
|
+
501: 'Not Implemented',
|
|
225
|
+
502: 'Bad Gateway',
|
|
226
|
+
503: 'Service Unavailable',
|
|
227
|
+
504: 'Gateway Timeout',
|
|
228
|
+
505: 'HTTP Version Not Supported',
|
|
229
|
+
506: 'Variant Also Negotiates',
|
|
230
|
+
507: 'Insufficient Storage',
|
|
231
|
+
508: 'Loop Detected',
|
|
232
|
+
510: 'Not Extended',
|
|
233
|
+
511: 'Network Authentication Required'
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return titles[status] || 'Error'
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* 将 Problem JSON 格式化为日志字符串
|
|
241
|
+
*/
|
|
242
|
+
export function formatProblemJsonLog(
|
|
243
|
+
problem: ProblemJson,
|
|
244
|
+
request: Request
|
|
245
|
+
): string {
|
|
246
|
+
const url = new URL(request.url)
|
|
247
|
+
const parts: string[] = []
|
|
248
|
+
|
|
249
|
+
// 标题行
|
|
250
|
+
const statusStr = problem.status.toString()
|
|
251
|
+
const emoji = getStatusEmoji(problem.status)
|
|
252
|
+
parts.push(`\n${emoji} [HTTP ${statusStr}] ${request.method} ${url.pathname}`)
|
|
253
|
+
|
|
254
|
+
// Problem JSON 内容
|
|
255
|
+
parts.push(` Type: ${problem.type}`)
|
|
256
|
+
parts.push(` Title: ${problem.title}`)
|
|
257
|
+
if (problem.detail) {
|
|
258
|
+
parts.push(` Detail: ${problem.detail}`)
|
|
259
|
+
}
|
|
260
|
+
if (problem.instance) {
|
|
261
|
+
parts.push(` Instance: ${problem.instance}`)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// 扩展字段
|
|
265
|
+
const extensions = Object.entries(problem).filter(
|
|
266
|
+
([key]) => !['type', 'title', 'status', 'detail', 'instance'].includes(key)
|
|
267
|
+
)
|
|
268
|
+
if (extensions.length > 0) {
|
|
269
|
+
parts.push(' Extensions:')
|
|
270
|
+
for (const [key, value] of extensions) {
|
|
271
|
+
parts.push(` ${key}: ${JSON.stringify(value)}`)
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return parts.join('\n')
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function getStatusEmoji(status: number): string {
|
|
279
|
+
if (status >= 500) return '🔴'
|
|
280
|
+
if (status >= 400) return '🟡'
|
|
281
|
+
if (status >= 300) return '🔵'
|
|
282
|
+
if (status >= 200) return '🟢'
|
|
283
|
+
return '⚪'
|
|
284
|
+
}
|