@thinkbun/middleware 1.0.0
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/README.md +290 -0
- package/package.json +19 -0
- package/src/index.ts +32 -0
- package/src/middlewares/cors.ts +182 -0
- package/src/middlewares/errorHandler.ts +88 -0
- package/src/middlewares/logger.ts +143 -0
- package/src/middlewares/static.ts +236 -0
- package/src/middlewares/validator.ts +187 -0
- package/src/test/index.ts +1 -0
- package/src/test/middlewares.test.ts +265 -0
- package/tsconfig.json +4 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import type { Context, Middleware } from '@thinkbun/core';
|
|
2
|
+
|
|
3
|
+
export interface LoggerOptions {
|
|
4
|
+
/**
|
|
5
|
+
* 日志格式
|
|
6
|
+
* @default 'text'
|
|
7
|
+
*/
|
|
8
|
+
format?: 'text' | 'json';
|
|
9
|
+
/**
|
|
10
|
+
* 自定义日志记录函数
|
|
11
|
+
* @param message 日志信息
|
|
12
|
+
* @param details 日志详细信息
|
|
13
|
+
*/
|
|
14
|
+
logger?: (message: string, details?: Record<string, any>) => void;
|
|
15
|
+
/**
|
|
16
|
+
* 需要忽略日志记录的路径
|
|
17
|
+
* @example ['/health', '/metrics']
|
|
18
|
+
*/
|
|
19
|
+
ignorePaths?: string[];
|
|
20
|
+
/**
|
|
21
|
+
* 是否记录请求体(仅适用于非GET请求)
|
|
22
|
+
* @default false
|
|
23
|
+
*/
|
|
24
|
+
logRequestBody?: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* 是否记录响应体
|
|
27
|
+
* @default false
|
|
28
|
+
*/
|
|
29
|
+
logResponseBody?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function logger(options: LoggerOptions = {}): Middleware {
|
|
33
|
+
const {
|
|
34
|
+
format = 'text',
|
|
35
|
+
logger = console.log,
|
|
36
|
+
ignorePaths = [],
|
|
37
|
+
logRequestBody = false,
|
|
38
|
+
logResponseBody = false,
|
|
39
|
+
} = options;
|
|
40
|
+
|
|
41
|
+
return async (ctx: Context, next: () => Promise<Response | Context | void>): Promise<Response | Context | void> => {
|
|
42
|
+
// 检查是否需要忽略该路径的日志
|
|
43
|
+
if (ignorePaths.includes(ctx.path)) {
|
|
44
|
+
return await next();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const startTime = Date.now();
|
|
48
|
+
const requestInfo: {
|
|
49
|
+
method: string;
|
|
50
|
+
path: string;
|
|
51
|
+
ip: string;
|
|
52
|
+
userAgent: string;
|
|
53
|
+
query: Record<string, string>;
|
|
54
|
+
body?: unknown;
|
|
55
|
+
} = {
|
|
56
|
+
method: ctx.method,
|
|
57
|
+
path: ctx.path,
|
|
58
|
+
ip: ctx.ip?.address || 'unknown',
|
|
59
|
+
userAgent: ctx.userAgent || 'unknown',
|
|
60
|
+
query: Object.fromEntries(ctx.url.searchParams),
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// 记录请求体(如果需要)
|
|
64
|
+
if (logRequestBody && ctx.method !== 'GET') {
|
|
65
|
+
try {
|
|
66
|
+
const body = await ctx.body();
|
|
67
|
+
requestInfo.body = body;
|
|
68
|
+
} catch (_error) {
|
|
69
|
+
// 忽略读取请求体错误
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let response: Response | Context | void = undefined;
|
|
74
|
+
try {
|
|
75
|
+
response = await next();
|
|
76
|
+
return response;
|
|
77
|
+
} catch (error) {
|
|
78
|
+
// 记录错误
|
|
79
|
+
const endTime = Date.now();
|
|
80
|
+
const errorInfo = {
|
|
81
|
+
...requestInfo,
|
|
82
|
+
status: 500,
|
|
83
|
+
duration: endTime - startTime,
|
|
84
|
+
error: (error as Error).message,
|
|
85
|
+
stack: (error as Error).stack,
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
if (format === 'json') {
|
|
89
|
+
logger(JSON.stringify(errorInfo));
|
|
90
|
+
} else {
|
|
91
|
+
logger(
|
|
92
|
+
`[ERROR] ${requestInfo.method} ${requestInfo.path} ${500} ${endTime - startTime}ms - ${(error as Error).message}`,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
throw error;
|
|
97
|
+
} finally {
|
|
98
|
+
// 记录正常响应
|
|
99
|
+
if (response instanceof Response) {
|
|
100
|
+
const endTime = Date.now();
|
|
101
|
+
const responseInfo: {
|
|
102
|
+
method: string;
|
|
103
|
+
path: string;
|
|
104
|
+
ip: string;
|
|
105
|
+
userAgent: string;
|
|
106
|
+
query: Record<string, string>;
|
|
107
|
+
body?: unknown;
|
|
108
|
+
status: number;
|
|
109
|
+
duration: number;
|
|
110
|
+
headers: Record<string, string>;
|
|
111
|
+
} = {
|
|
112
|
+
...requestInfo,
|
|
113
|
+
status: response.status,
|
|
114
|
+
duration: endTime - startTime,
|
|
115
|
+
headers: Object.fromEntries(response.headers),
|
|
116
|
+
body: undefined,
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// 记录响应体(如果需要)
|
|
120
|
+
if (logResponseBody) {
|
|
121
|
+
try {
|
|
122
|
+
// 克隆响应以读取响应体
|
|
123
|
+
const clonedResponse = response.clone();
|
|
124
|
+
const body = await clonedResponse.text();
|
|
125
|
+
try {
|
|
126
|
+
responseInfo.body = JSON.parse(body);
|
|
127
|
+
} catch {
|
|
128
|
+
responseInfo.body = body;
|
|
129
|
+
}
|
|
130
|
+
} catch (_error) {
|
|
131
|
+
// 忽略读取响应体错误
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (format === 'json') {
|
|
136
|
+
logger(JSON.stringify(responseInfo));
|
|
137
|
+
} else {
|
|
138
|
+
logger(`[INFO] ${requestInfo.method} ${requestInfo.path} ${response.status} ${endTime - startTime}ms`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
import type { Context, Middleware } from '@thinkbun/core';
|
|
5
|
+
|
|
6
|
+
export interface StaticOptions {
|
|
7
|
+
/**
|
|
8
|
+
* 静态资源目录的绝对路径
|
|
9
|
+
*/
|
|
10
|
+
root: string;
|
|
11
|
+
/**
|
|
12
|
+
* 请求路径前缀
|
|
13
|
+
* @example '/public' 会将 /public/css/style.css 映射到 root/css/style.css
|
|
14
|
+
*/
|
|
15
|
+
prefix?: string;
|
|
16
|
+
/**
|
|
17
|
+
* 自动索引文件
|
|
18
|
+
* @default ['index.html', 'index.htm']
|
|
19
|
+
*/
|
|
20
|
+
index?: string[];
|
|
21
|
+
/**
|
|
22
|
+
* 缓存控制头
|
|
23
|
+
* @example 'public, max-age=31536000' 或 (path: string) => string
|
|
24
|
+
*/
|
|
25
|
+
cacheControl?: string | ((filePath: string) => string);
|
|
26
|
+
/**
|
|
27
|
+
* 是否启用文件压缩
|
|
28
|
+
* @default true
|
|
29
|
+
*/
|
|
30
|
+
compression?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* 文件不存在时的处理函数
|
|
33
|
+
* @param ctx 请求上下文
|
|
34
|
+
* @param filePath 请求的文件路径
|
|
35
|
+
* @returns 返回自定义的Response对象或undefined继续执行后续中间件
|
|
36
|
+
*/
|
|
37
|
+
notFoundHandler?: (ctx: Context, filePath: string) => Response | undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function staticServe(options: StaticOptions): Middleware {
|
|
41
|
+
if (!options.root) {
|
|
42
|
+
throw new Error('Static middleware requires a root directory');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const {
|
|
46
|
+
root,
|
|
47
|
+
prefix = '',
|
|
48
|
+
index = ['index.html', 'index.htm'],
|
|
49
|
+
cacheControl,
|
|
50
|
+
_compression = true,
|
|
51
|
+
notFoundHandler,
|
|
52
|
+
} = options;
|
|
53
|
+
|
|
54
|
+
// 移除前缀末尾的斜杠
|
|
55
|
+
const normalizedPrefix = prefix.endsWith('/') ? prefix.slice(0, -1) : prefix;
|
|
56
|
+
|
|
57
|
+
// 获取文件的MIME类型
|
|
58
|
+
const getContentType = (filePath: string): string => {
|
|
59
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
60
|
+
switch (ext) {
|
|
61
|
+
case '.html':
|
|
62
|
+
return 'text/html; charset=utf-8';
|
|
63
|
+
case '.css':
|
|
64
|
+
return 'text/css';
|
|
65
|
+
case '.js':
|
|
66
|
+
return 'application/javascript; charset=utf-8';
|
|
67
|
+
case '.json':
|
|
68
|
+
return 'application/json';
|
|
69
|
+
case '.png':
|
|
70
|
+
return 'image/png';
|
|
71
|
+
case '.jpg':
|
|
72
|
+
case '.jpeg':
|
|
73
|
+
return 'image/jpeg';
|
|
74
|
+
case '.gif':
|
|
75
|
+
return 'image/gif';
|
|
76
|
+
case '.svg':
|
|
77
|
+
return 'image/svg+xml';
|
|
78
|
+
case '.ico':
|
|
79
|
+
return 'image/x-icon';
|
|
80
|
+
case '.webp':
|
|
81
|
+
return 'image/webp';
|
|
82
|
+
case '.woff':
|
|
83
|
+
return 'font/woff';
|
|
84
|
+
case '.woff2':
|
|
85
|
+
return 'font/woff2';
|
|
86
|
+
case '.ttf':
|
|
87
|
+
return 'font/ttf';
|
|
88
|
+
case '.otf':
|
|
89
|
+
return 'font/otf';
|
|
90
|
+
case '.eot':
|
|
91
|
+
return 'application/vnd.ms-fontobject';
|
|
92
|
+
case '.mp3':
|
|
93
|
+
return 'audio/mpeg';
|
|
94
|
+
case '.mp4':
|
|
95
|
+
return 'video/mp4';
|
|
96
|
+
case '.pdf':
|
|
97
|
+
return 'application/pdf';
|
|
98
|
+
case '.zip':
|
|
99
|
+
return 'application/zip';
|
|
100
|
+
case '.txt':
|
|
101
|
+
return 'text/plain; charset=utf-8';
|
|
102
|
+
default:
|
|
103
|
+
return 'application/octet-stream';
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
return async (ctx: Context, next: () => Promise<Response | Context | void>): Promise<Response | Context | void> => {
|
|
108
|
+
// 检查请求路径是否匹配前缀
|
|
109
|
+
if (normalizedPrefix && !ctx.path.startsWith(normalizedPrefix)) {
|
|
110
|
+
return await next();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 移除前缀,获取相对文件路径
|
|
114
|
+
let filePath = ctx.path;
|
|
115
|
+
if (normalizedPrefix) {
|
|
116
|
+
filePath = filePath.slice(normalizedPrefix.length);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// 防止路径遍历攻击
|
|
120
|
+
if (filePath.includes('..')) {
|
|
121
|
+
return new Response('Forbidden', { status: 403 });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// 构建绝对文件路径
|
|
125
|
+
let absolutePath = path.join(root, filePath);
|
|
126
|
+
|
|
127
|
+
// 检查是否是目录
|
|
128
|
+
let stats;
|
|
129
|
+
try {
|
|
130
|
+
stats = await fs.stat(absolutePath);
|
|
131
|
+
} catch (_error) {
|
|
132
|
+
// 文件不存在,检查是否有自定义404处理
|
|
133
|
+
if (notFoundHandler) {
|
|
134
|
+
const customResponse = notFoundHandler(ctx, absolutePath);
|
|
135
|
+
if (customResponse) {
|
|
136
|
+
return customResponse;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return await next();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// 如果是目录,尝试查找索引文件
|
|
143
|
+
if (stats.isDirectory()) {
|
|
144
|
+
// 确保路径以斜杠结尾
|
|
145
|
+
if (!ctx.path.endsWith('/')) {
|
|
146
|
+
return new Response(null, {
|
|
147
|
+
status: 301,
|
|
148
|
+
headers: {
|
|
149
|
+
Location: ctx.path + '/',
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 查找索引文件
|
|
155
|
+
for (const indexFile of index) {
|
|
156
|
+
const indexPath = path.join(absolutePath, indexFile);
|
|
157
|
+
try {
|
|
158
|
+
const indexStats = await fs.stat(indexPath);
|
|
159
|
+
if (indexStats.isFile()) {
|
|
160
|
+
absolutePath = indexPath;
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
} catch (_error) {
|
|
164
|
+
// 继续查找下一个索引文件
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// 检查是否找到了索引文件
|
|
169
|
+
try {
|
|
170
|
+
stats = await fs.stat(absolutePath);
|
|
171
|
+
if (!stats.isFile()) {
|
|
172
|
+
// 没有找到索引文件,继续执行后续中间件
|
|
173
|
+
return await next();
|
|
174
|
+
}
|
|
175
|
+
} catch (_error) {
|
|
176
|
+
return await next();
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// 读取文件
|
|
181
|
+
let content: Buffer;
|
|
182
|
+
try {
|
|
183
|
+
content = await fs.readFile(absolutePath);
|
|
184
|
+
} catch (_error) {
|
|
185
|
+
return await next();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// 设置响应头
|
|
189
|
+
const headers: Record<string, string> = {
|
|
190
|
+
'Content-Type': getContentType(absolutePath),
|
|
191
|
+
'Content-Length': content.length.toString(),
|
|
192
|
+
'Last-Modified': stats.mtime.toUTCString(),
|
|
193
|
+
'Accept-Ranges': 'bytes',
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
// 设置缓存控制头
|
|
197
|
+
if (cacheControl) {
|
|
198
|
+
const cacheHeader = typeof cacheControl === 'function' ? cacheControl(absolutePath) : cacheControl;
|
|
199
|
+
if (cacheHeader) {
|
|
200
|
+
headers['Cache-Control'] = cacheHeader;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// 支持范围请求
|
|
205
|
+
const rangeHeader = ctx.header('Range');
|
|
206
|
+
if (rangeHeader) {
|
|
207
|
+
const parts = rangeHeader.replace(/bytes=/, '').split('-');
|
|
208
|
+
const start = parseInt(parts[0] || '0', 10);
|
|
209
|
+
const end = parts[1] ? parseInt(parts[1], 10) : content.length - 1;
|
|
210
|
+
|
|
211
|
+
if (isNaN(start) || isNaN(end) || start > end || end >= content.length) {
|
|
212
|
+
return new Response(null, {
|
|
213
|
+
status: 416,
|
|
214
|
+
headers: {
|
|
215
|
+
'Content-Range': `bytes */${content.length}`,
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const rangeContent = content.slice(start, end + 1);
|
|
221
|
+
headers['Content-Range'] = `bytes ${start}-${end}/${content.length}`;
|
|
222
|
+
headers['Content-Length'] = rangeContent.length.toString();
|
|
223
|
+
|
|
224
|
+
return new Response(rangeContent, {
|
|
225
|
+
status: 206,
|
|
226
|
+
headers,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// 返回完整文件
|
|
231
|
+
return new Response(content, {
|
|
232
|
+
status: 200,
|
|
233
|
+
headers,
|
|
234
|
+
});
|
|
235
|
+
};
|
|
236
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { BadRequestError, type Context, type Middleware, type RouteSchema } from '@thinkbun/core';
|
|
2
|
+
import Ajv from 'ajv';
|
|
3
|
+
import addFormats from 'ajv-formats';
|
|
4
|
+
|
|
5
|
+
// 使用与核心框架相同的Ajv配置
|
|
6
|
+
const ajv = addFormats(
|
|
7
|
+
new Ajv({
|
|
8
|
+
coerceTypes: true, // 自动类型转换
|
|
9
|
+
useDefaults: true, // 使用默认值
|
|
10
|
+
removeAdditional: false, // 保留额外属性
|
|
11
|
+
allErrors: true, // 返回所有错误
|
|
12
|
+
}),
|
|
13
|
+
[
|
|
14
|
+
'date-time',
|
|
15
|
+
'time',
|
|
16
|
+
'date',
|
|
17
|
+
'email',
|
|
18
|
+
'hostname',
|
|
19
|
+
'ipv4',
|
|
20
|
+
'ipv6',
|
|
21
|
+
'uri',
|
|
22
|
+
'uri-reference',
|
|
23
|
+
'uuid',
|
|
24
|
+
'uri-template',
|
|
25
|
+
'json-pointer',
|
|
26
|
+
'relative-json-pointer',
|
|
27
|
+
'regex',
|
|
28
|
+
],
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
export interface ValidationRule {
|
|
32
|
+
/**
|
|
33
|
+
* 路由路径
|
|
34
|
+
* @example '/api/users'
|
|
35
|
+
*/
|
|
36
|
+
path: string;
|
|
37
|
+
/**
|
|
38
|
+
* HTTP方法
|
|
39
|
+
* @example 'POST'
|
|
40
|
+
*/
|
|
41
|
+
method: string;
|
|
42
|
+
/**
|
|
43
|
+
* 验证模式
|
|
44
|
+
*/
|
|
45
|
+
schema: RouteSchema;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface ValidatorOptions {
|
|
49
|
+
/**
|
|
50
|
+
* 验证规则列表
|
|
51
|
+
*/
|
|
52
|
+
rules: ValidationRule[];
|
|
53
|
+
/**
|
|
54
|
+
* 自定义错误处理函数
|
|
55
|
+
* @param error 验证错误
|
|
56
|
+
* @param ctx 请求上下文
|
|
57
|
+
* @returns 返回自定义的Response对象或抛出错误使用默认处理
|
|
58
|
+
*/
|
|
59
|
+
errorHandler?: (error: Error, ctx: Context) => Response | never;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function validator(options: ValidatorOptions): Middleware {
|
|
63
|
+
if (!options.rules || options.rules.length === 0) {
|
|
64
|
+
throw new Error('Validator middleware requires at least one validation rule');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 编译所有验证规则
|
|
68
|
+
const compiledRules = options.rules.map((rule) => {
|
|
69
|
+
const compiled: any = {};
|
|
70
|
+
|
|
71
|
+
if (rule.schema.query) {
|
|
72
|
+
compiled.query = ajv.compile(rule.schema.query);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (rule.schema.body) {
|
|
76
|
+
compiled.body = ajv.compile(rule.schema.body);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (rule.schema.params) {
|
|
80
|
+
compiled.params = ajv.compile(rule.schema.params);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (rule.schema.headers) {
|
|
84
|
+
compiled.headers = ajv.compile(rule.schema.headers);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
...rule,
|
|
89
|
+
compiled,
|
|
90
|
+
};
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
return async (ctx: Context, next: () => Promise<Response | Context | void>): Promise<Response | Context | void> => {
|
|
94
|
+
// 查找匹配的验证规则
|
|
95
|
+
const matchingRules = compiledRules.filter(
|
|
96
|
+
(rule) => rule.path === ctx.path && rule.method.toUpperCase() === ctx.method.toUpperCase(),
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
if (matchingRules.length === 0) {
|
|
100
|
+
// 没有匹配的验证规则,继续执行后续中间件
|
|
101
|
+
return await next();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 使用第一个匹配的规则
|
|
105
|
+
const rule = matchingRules[0]!; // 类型断言,因为已经确保matchingRules.length > 0
|
|
106
|
+
const errors: any[] = [];
|
|
107
|
+
|
|
108
|
+
// 验证查询参数
|
|
109
|
+
if (rule.compiled.query) {
|
|
110
|
+
const query = Object.fromEntries(ctx.url.searchParams);
|
|
111
|
+
if (!rule.compiled.query(query)) {
|
|
112
|
+
errors.push(
|
|
113
|
+
...rule.compiled.query.errors!.map((err: any) => ({
|
|
114
|
+
...err,
|
|
115
|
+
source: 'query',
|
|
116
|
+
})),
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// 验证路由参数
|
|
122
|
+
if (rule.compiled.params) {
|
|
123
|
+
if (!rule.compiled.params(ctx.params)) {
|
|
124
|
+
errors.push(
|
|
125
|
+
...rule.compiled.params.errors!.map((err: any) => ({
|
|
126
|
+
...err,
|
|
127
|
+
source: 'params',
|
|
128
|
+
})),
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// 验证请求头
|
|
134
|
+
if (rule.compiled.headers) {
|
|
135
|
+
const headers = Object.fromEntries(ctx.headers);
|
|
136
|
+
if (!rule.compiled.headers(headers)) {
|
|
137
|
+
errors.push(
|
|
138
|
+
...rule.compiled.headers.errors!.map((err: any) => ({
|
|
139
|
+
...err,
|
|
140
|
+
source: 'headers',
|
|
141
|
+
})),
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// 验证请求体
|
|
147
|
+
if (rule.compiled.body && ctx.method !== 'GET' && ctx.method !== 'HEAD') {
|
|
148
|
+
try {
|
|
149
|
+
const body = await ctx.body();
|
|
150
|
+
if (!rule.compiled.body(body)) {
|
|
151
|
+
errors.push(
|
|
152
|
+
...rule.compiled.body.errors!.map((err: any) => ({
|
|
153
|
+
...err,
|
|
154
|
+
source: 'body',
|
|
155
|
+
})),
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
} catch (error) {
|
|
159
|
+
// 如果无法解析请求体,添加错误信息
|
|
160
|
+
errors.push({
|
|
161
|
+
keyword: 'invalid_body',
|
|
162
|
+
message: 'Invalid request body',
|
|
163
|
+
source: 'body',
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// 如果有验证错误
|
|
169
|
+
if (errors.length > 0) {
|
|
170
|
+
const error = new BadRequestError(errors);
|
|
171
|
+
|
|
172
|
+
// 使用自定义错误处理(如果提供)
|
|
173
|
+
if (options.errorHandler) {
|
|
174
|
+
const response = options.errorHandler(error, ctx);
|
|
175
|
+
if (response) {
|
|
176
|
+
return response;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// 抛出错误,由错误处理中间件处理
|
|
181
|
+
throw error;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// 验证通过,继续执行后续中间件
|
|
185
|
+
return await next();
|
|
186
|
+
};
|
|
187
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
// Test entry point
|