@pori15/logixlysia 6.0.1 → 6.0.3

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