@thinkbun/middleware 1.0.2 → 1.0.4
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/CHANGELOG.md +16 -0
- package/README.md +10 -84
- package/README_CN.md +290 -0
- package/package.json +2 -2
- package/src/middlewares/cors.ts +55 -31
- package/src/middlewares/errorHandler.ts +46 -26
- package/src/middlewares/logger.ts +73 -25
- package/src/test/middlewares.test.ts +64 -58
- package/tsconfig.json +1 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# @thinkbun/middleware
|
|
2
2
|
|
|
3
|
+
## 1.0.4
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- docs
|
|
8
|
+
- Updated dependencies
|
|
9
|
+
- @thinkbun/core@1.0.7
|
|
10
|
+
|
|
11
|
+
## 1.0.3
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- feat(core): 增强 CLI 应用和核心框架功能
|
|
16
|
+
- Updated dependencies
|
|
17
|
+
- @thinkbun/core@1.0.6
|
|
18
|
+
|
|
3
19
|
## 1.0.2
|
|
4
20
|
|
|
5
21
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -78,81 +78,7 @@ app.use(logger({
|
|
|
78
78
|
| `logger` | `(message: string) => void` | `console.log` | Custom logger function |
|
|
79
79
|
| `ignorePaths` | `string[]` | `[]` | Array of paths to exclude from logging |
|
|
80
80
|
|
|
81
|
-
### 3.
|
|
82
|
-
|
|
83
|
-
Request parameter validation middleware that validates request body, query parameters, and route parameters using Ajv schema validation.
|
|
84
|
-
|
|
85
|
-
#### Usage
|
|
86
|
-
|
|
87
|
-
```typescript
|
|
88
|
-
import { validator } from '@thinkbun/middleware';
|
|
89
|
-
|
|
90
|
-
app.use(validator({
|
|
91
|
-
rules: [
|
|
92
|
-
{
|
|
93
|
-
path: '/api/users',
|
|
94
|
-
method: 'POST',
|
|
95
|
-
schema: {
|
|
96
|
-
body: {
|
|
97
|
-
type: 'object',
|
|
98
|
-
properties: {
|
|
99
|
-
name: { type: 'string' },
|
|
100
|
-
email: { type: 'string', format: 'email' },
|
|
101
|
-
age: { type: 'number', minimum: 18 }
|
|
102
|
-
},
|
|
103
|
-
required: ['name', 'email']
|
|
104
|
-
},
|
|
105
|
-
query: {
|
|
106
|
-
type: 'object',
|
|
107
|
-
properties: {
|
|
108
|
-
active: { type: 'boolean' }
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
},
|
|
113
|
-
{
|
|
114
|
-
path: '/api/users/:id',
|
|
115
|
-
method: 'GET',
|
|
116
|
-
schema: {
|
|
117
|
-
params: {
|
|
118
|
-
type: 'object',
|
|
119
|
-
properties: {
|
|
120
|
-
id: { type: 'string', pattern: '^\\d+$' }
|
|
121
|
-
},
|
|
122
|
-
required: ['id']
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
],
|
|
127
|
-
errorHandler: (errors, ctx) => {
|
|
128
|
-
// Custom validation error handling
|
|
129
|
-
ctx.throw(400, `Validation error: ${errors[0].message}`);
|
|
130
|
-
}
|
|
131
|
-
}));
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
#### Options
|
|
135
|
-
|
|
136
|
-
| Option | Type | Default | Description |
|
|
137
|
-
| -------------- | --------------------------------------- | ------- | ----------------------------------------- |
|
|
138
|
-
| `rules` | `ValidationRule[]` | - | Array of validation rules |
|
|
139
|
-
| `errorHandler` | `(errors: any[], ctx: Context) => void` | - | Custom validation error handling function |
|
|
140
|
-
|
|
141
|
-
#### ValidationRule Interface
|
|
142
|
-
|
|
143
|
-
```typescript
|
|
144
|
-
interface ValidationRule {
|
|
145
|
-
path: string; // Request path pattern to match
|
|
146
|
-
method: string; // HTTP method to match (GET, POST, etc.)
|
|
147
|
-
schema: {
|
|
148
|
-
body?: any; // Request body schema
|
|
149
|
-
query?: any; // Query parameters schema
|
|
150
|
-
params?: any; // Route parameters schema
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
### 4. CORS
|
|
81
|
+
### 3. CORS
|
|
156
82
|
|
|
157
83
|
CORS configuration middleware that handles Cross-Origin Resource Sharing, supporting flexible configuration options.
|
|
158
84
|
|
|
@@ -188,9 +114,9 @@ app.use(cors({
|
|
|
188
114
|
| `maxAge` | `number` | `86400` (24 hours) | Preflight request cache time (seconds) |
|
|
189
115
|
| `preflightContinue` | `boolean` | `true` | Whether to continue processing preflight requests |
|
|
190
116
|
|
|
191
|
-
###
|
|
117
|
+
### 4. Static Serve
|
|
192
118
|
|
|
193
|
-
Static file serving middleware that serves static files from
|
|
119
|
+
Static file serving middleware that serves static files from specified directory, with support for request path prefixes, cache control, and more.
|
|
194
120
|
|
|
195
121
|
#### Usage
|
|
196
122
|
|
|
@@ -215,10 +141,10 @@ app.use(staticServe({
|
|
|
215
141
|
#### Options
|
|
216
142
|
|
|
217
143
|
| Option | Type | Default | Description |
|
|
218
|
-
| -------------- | --------- | -------------- |
|
|
219
|
-
| `root`
|
|
220
|
-
| `prefix`
|
|
221
|
-
| `index`
|
|
144
|
+
| -------------- | --------- | -------------- | ------------------------- |
|
|
145
|
+
| `root` | `string` | - | Static file directory (required) |
|
|
146
|
+
| `prefix` | `string` | `''` | Request path prefix |
|
|
147
|
+
| `index` | `string` | `'index.html'` | Default index file |
|
|
222
148
|
| `maxAge` | `number` | `3600` | Cache control max-age (seconds) |
|
|
223
149
|
| `gzip` | `boolean` | `true` | Enable gzip compression |
|
|
224
150
|
| `brotli` | `boolean` | `true` | Enable Brotli compression |
|
|
@@ -240,7 +166,7 @@ app.use(middlewares.staticServe({ root: './public' }));
|
|
|
240
166
|
|
|
241
167
|
## Middleware Execution Flow
|
|
242
168
|
|
|
243
|
-
The middleware follows
|
|
169
|
+
The middleware follows Koa onion model, where each middleware can execute code before and after subsequent middleware:
|
|
244
170
|
|
|
245
171
|
```typescript
|
|
246
172
|
app.use(async (ctx, next) => {
|
|
@@ -275,11 +201,11 @@ app.use(async (ctx, next) => {
|
|
|
275
201
|
|
|
276
202
|
## Best Practices
|
|
277
203
|
|
|
278
|
-
1. **Order Matters**: Middleware execution follows
|
|
204
|
+
1. **Order Matters**: Middleware execution follows order in which they are added with `app.use()`. Place global middleware (like error handler, logger, CORS) before route-specific middleware.
|
|
279
205
|
|
|
280
206
|
2. **Error Handler Placement**: Always place the error handler middleware first to ensure it can capture errors from all subsequent middleware.
|
|
281
207
|
|
|
282
|
-
3. **Static Files**: Place
|
|
208
|
+
3. **Static Files**: Place static file serving middleware before route handlers to improve performance (static files are served directly without going through route processing).
|
|
283
209
|
|
|
284
210
|
4. **Validation Scope**: Use the validator middleware with specific paths and methods to avoid unnecessary validation overhead.
|
|
285
211
|
|
package/README_CN.md
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
# ThinkBun 中间件集合
|
|
2
|
+
|
|
3
|
+
为 ThinkBun 框架提供的全面中间件集合,遵循 Koa 中间件规范,支持 async/await 语法和洋葱模型执行流程。
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun add @thinkbun/middleware
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## 可用中间件
|
|
12
|
+
|
|
13
|
+
### 1. 错误处理器
|
|
14
|
+
|
|
15
|
+
全局错误处理中间件,捕获并处理后续中间件抛出的错误,支持在开发和生产环境中显示不同的错误信息。
|
|
16
|
+
|
|
17
|
+
#### 使用方法
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
import { errorHandler } from '@thinkbun/middleware';
|
|
21
|
+
|
|
22
|
+
// 基本使用
|
|
23
|
+
app.use(errorHandler());
|
|
24
|
+
|
|
25
|
+
// 使用自定义选项
|
|
26
|
+
app.use(errorHandler({
|
|
27
|
+
exposeStackTrace: true, // 在错误响应中显示堆栈跟踪(仅建议在开发环境使用)
|
|
28
|
+
customHandler: (error, ctx) => {
|
|
29
|
+
// 自定义错误处理逻辑
|
|
30
|
+
return new Response(JSON.stringify({ error: error.message }), {
|
|
31
|
+
status: (error as any).status || 500,
|
|
32
|
+
headers: { 'Content-Type': 'application/json' }
|
|
33
|
+
});
|
|
34
|
+
},
|
|
35
|
+
logger: (error) => {
|
|
36
|
+
// 自定义错误日志记录
|
|
37
|
+
console.error('Error occurred:', error);
|
|
38
|
+
}
|
|
39
|
+
}));
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
#### 选项
|
|
43
|
+
|
|
44
|
+
| 选项 | 类型 | 默认值 | 描述 |
|
|
45
|
+
| ----------------- | ------------------------------------------ | ------- | -------------------------------------- |
|
|
46
|
+
| `exposeStackTrace` | `boolean` | `false` | 是否在错误响应中暴露堆栈跟踪 |
|
|
47
|
+
| `customHandler` | `(error: Error, ctx: Context) => Response` | - | 自定义错误处理函数 |
|
|
48
|
+
| `logger` | `(error: Error, ctx: Context) => void` | - | 自定义错误日志记录函数 |
|
|
49
|
+
|
|
50
|
+
### 2. 日志记录器
|
|
51
|
+
|
|
52
|
+
请求日志记录中间件,记录请求和响应信息,支持自定义日志格式、路径过滤和日志记录器。
|
|
53
|
+
|
|
54
|
+
#### 使用方法
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
import { logger } from '@thinkbun/middleware';
|
|
58
|
+
|
|
59
|
+
// 基本使用
|
|
60
|
+
app.use(logger());
|
|
61
|
+
|
|
62
|
+
// 使用自定义选项
|
|
63
|
+
app.use(logger({
|
|
64
|
+
format: (ctx) => `${ctx.method} ${ctx.path} ${ctx.header('user-agent')}`,
|
|
65
|
+
logger: (message) => {
|
|
66
|
+
// 自定义日志记录
|
|
67
|
+
console.log('[LOG]', message);
|
|
68
|
+
},
|
|
69
|
+
ignorePaths: ['/health', '/metrics'] // 从日志中排除的路径
|
|
70
|
+
}));
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
#### 选项
|
|
74
|
+
|
|
75
|
+
| 选项 | 类型 | 默认值 | 描述 |
|
|
76
|
+
| ------------ | --------------------------- | ------------- | ------------------------- |
|
|
77
|
+
| `format` | `(ctx: Context) => string` | - | 自定义日志格式函数 |
|
|
78
|
+
| `logger` | `(message: string) => void` | `console.log` | 自定义日志记录函数 |
|
|
79
|
+
| `ignorePaths`| `string[]` | `[]` | 从日志中排除的路径数组 |
|
|
80
|
+
|
|
81
|
+
### 3. 验证器
|
|
82
|
+
|
|
83
|
+
请求参数验证中间件,使用 Ajv 模式验证验证请求体、查询参数和路由参数。
|
|
84
|
+
|
|
85
|
+
#### 使用方法
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
import { validator } from '@thinkbun/middleware';
|
|
89
|
+
|
|
90
|
+
app.use(validator({
|
|
91
|
+
rules: [
|
|
92
|
+
{
|
|
93
|
+
path: '/api/users',
|
|
94
|
+
method: 'POST',
|
|
95
|
+
schema: {
|
|
96
|
+
body: {
|
|
97
|
+
type: 'object',
|
|
98
|
+
properties: {
|
|
99
|
+
name: { type: 'string' },
|
|
100
|
+
email: { type: 'string', format: 'email' },
|
|
101
|
+
age: { type: 'number', minimum: 18 }
|
|
102
|
+
},
|
|
103
|
+
required: ['name', 'email']
|
|
104
|
+
},
|
|
105
|
+
query: {
|
|
106
|
+
type: 'object',
|
|
107
|
+
properties: {
|
|
108
|
+
active: { type: 'boolean' }
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
path: '/api/users/:id',
|
|
115
|
+
method: 'GET',
|
|
116
|
+
schema: {
|
|
117
|
+
params: {
|
|
118
|
+
type: 'object',
|
|
119
|
+
properties: {
|
|
120
|
+
id: { type: 'string', pattern: '^\\d+$' }
|
|
121
|
+
},
|
|
122
|
+
required: ['id']
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
],
|
|
127
|
+
errorHandler: (errors, ctx) => {
|
|
128
|
+
// 自定义验证错误处理
|
|
129
|
+
ctx.throw(400, `Validation error: ${errors[0].message}`);
|
|
130
|
+
}
|
|
131
|
+
}));
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
#### 选项
|
|
135
|
+
|
|
136
|
+
| 选项 | 类型 | 默认值 | 描述 |
|
|
137
|
+
| -------------- | --------------------------------------- | ------- | ----------------------------------- |
|
|
138
|
+
| `rules` | `ValidationRule[]` | - | 验证规则数组 |
|
|
139
|
+
| `errorHandler` | `(errors: any[], ctx: Context) => void` | - | 自定义验证错误处理函数 |
|
|
140
|
+
|
|
141
|
+
#### ValidationRule 接口
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
interface ValidationRule {
|
|
145
|
+
path: string; // 要匹配的请求路径模式
|
|
146
|
+
method: string; // 要匹配的 HTTP 方法(GET、POST 等)
|
|
147
|
+
schema: {
|
|
148
|
+
body?: any; // 请求体模式
|
|
149
|
+
query?: any; // 查询参数模式
|
|
150
|
+
params?: any; // 路由参数模式
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### 4. CORS
|
|
156
|
+
|
|
157
|
+
CORS 配置中间件,处理跨源资源共享,支持灵活的配置选项。
|
|
158
|
+
|
|
159
|
+
#### 使用方法
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
import { cors } from '@thinkbun/middleware';
|
|
163
|
+
|
|
164
|
+
// 基本使用(默认允许所有源)
|
|
165
|
+
app.use(cors());
|
|
166
|
+
|
|
167
|
+
// 使用自定义选项
|
|
168
|
+
app.use(cors({
|
|
169
|
+
origin: ['https://example.com', 'https://sub.example.com'], // 允许特定源
|
|
170
|
+
methods: ['GET', 'POST', 'PUT', 'DELETE'], // 允许的 HTTP 方法
|
|
171
|
+
allowedHeaders: ['Content-Type', 'Authorization'], // 允许的请求头
|
|
172
|
+
exposedHeaders: ['X-Custom-Header'], // 暴露给客户端的响应头
|
|
173
|
+
credentials: true, // 允许凭据
|
|
174
|
+
maxAge: 86400, // 预检请求缓存时间(秒)
|
|
175
|
+
preflightContinue: true // 继续处理预检请求
|
|
176
|
+
}));
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
#### 选项
|
|
180
|
+
|
|
181
|
+
| 选项 | 类型 | 默认值 | 描述 |
|
|
182
|
+
| ----------------- | ----------------------------------------------------- | -------------------------------------------------------- | ------------------------------------------------------------------- |
|
|
183
|
+
| `origin` | `string \| string[] \| ((origin: string) => boolean)` | `'*'` | 允许的源 |
|
|
184
|
+
| `methods` | `string[]` | `['GET', 'HEAD', 'PUT', 'POST', 'DELETE', 'PATCH']` | 允许的 HTTP 方法 |
|
|
185
|
+
| `allowedHeaders` | `string[]` | - | 允许的请求头(默认为 Access-Control-Request-Headers 的值) |
|
|
186
|
+
| `exposedHeaders` | `string[]` | - | 暴露给客户端的响应头 |
|
|
187
|
+
| `credentials` | `boolean` | `false` | 是否允许凭据 |
|
|
188
|
+
| `maxAge` | `number` | `86400` (24 小时) | 预检请求缓存时间(秒) |
|
|
189
|
+
| `preflightContinue` | `boolean` | `true` | 是否继续处理预检请求 |
|
|
190
|
+
|
|
191
|
+
### 5. 静态文件服务
|
|
192
|
+
|
|
193
|
+
静态文件服务中间件,从指定目录服务静态文件,支持请求路径前缀、缓存控制等功能。
|
|
194
|
+
|
|
195
|
+
#### 使用方法
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
import { staticServe } from '@thinkbun/middleware';
|
|
199
|
+
|
|
200
|
+
// 基本使用
|
|
201
|
+
app.use(staticServe({ root: './public' }));
|
|
202
|
+
|
|
203
|
+
// 使用自定义选项
|
|
204
|
+
app.use(staticServe({
|
|
205
|
+
root: './public', // 静态文件目录
|
|
206
|
+
prefix: '/static', // 请求路径前缀(例如 /static/css/style.css)
|
|
207
|
+
index: 'index.html', // 默认索引文件
|
|
208
|
+
maxAge: 3600, // 缓存控制 max-age(秒)
|
|
209
|
+
gzip: true, // 启用 gzip 压缩
|
|
210
|
+
brotli: true, // 启用 Brotli 压缩
|
|
211
|
+
cacheControl: true // 启用缓存控制头
|
|
212
|
+
}));
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
#### 选项
|
|
216
|
+
|
|
217
|
+
| 选项 | 类型 | 默认值 | 描述 |
|
|
218
|
+
| ------------- | --------- | -------------- | ------------------------- |
|
|
219
|
+
| `root` | `string` | - | 静态文件目录(必需) |
|
|
220
|
+
| `prefix` | `string` | `''` | 请求路径前缀 |
|
|
221
|
+
| `index` | `string` | `'index.html'` | 默认索引文件 |
|
|
222
|
+
| `maxAge` | `number` | `3600` | 缓存控制 max-age(秒) |
|
|
223
|
+
| `gzip` | `boolean` | `true` | 启用 gzip 压缩 |
|
|
224
|
+
| `brotli` | `boolean` | `true` | 启用 Brotli 压缩 |
|
|
225
|
+
| `cacheControl` | `boolean` | `true` | 启用缓存控制头 |
|
|
226
|
+
|
|
227
|
+
## 使用所有中间件
|
|
228
|
+
|
|
229
|
+
您也可以一次性导入所有中间件:
|
|
230
|
+
|
|
231
|
+
```typescript
|
|
232
|
+
import { middlewares } from '@thinkbun/middleware';
|
|
233
|
+
|
|
234
|
+
// 使用所有中间件
|
|
235
|
+
app.use(middlewares.errorHandler());
|
|
236
|
+
app.use(middlewares.logger());
|
|
237
|
+
app.use(middlewares.cors());
|
|
238
|
+
app.use(middlewares.staticServe({ root: './public' }));
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## 中间件执行流程
|
|
242
|
+
|
|
243
|
+
中间件遵循 Koa 洋葱模型,每个中间件可以在后续中间件之前和之后执行代码:
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
app.use(async (ctx, next) => {
|
|
247
|
+
// 在下一个中间件之前执行的代码
|
|
248
|
+
console.log('中间件 1: 之前');
|
|
249
|
+
|
|
250
|
+
const response = await next();
|
|
251
|
+
|
|
252
|
+
// 在下一个中间件之后执行的代码
|
|
253
|
+
console.log('中间件 1: 之后');
|
|
254
|
+
|
|
255
|
+
return response;
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
app.use(async (ctx, next) => {
|
|
259
|
+
console.log('中间件 2: 之前');
|
|
260
|
+
|
|
261
|
+
const response = await next();
|
|
262
|
+
|
|
263
|
+
console.log('中间件 2: 之后');
|
|
264
|
+
|
|
265
|
+
return response;
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// 处理请求时的输出:
|
|
269
|
+
// 中间件 1: 之前
|
|
270
|
+
// 中间件 2: 之前
|
|
271
|
+
// (请求处理)
|
|
272
|
+
// 中间件 2: 之后
|
|
273
|
+
// 中间件 1: 之后
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
## 最佳实践
|
|
277
|
+
|
|
278
|
+
1. **顺序很重要**:中间件执行遵循使用 `app.use()` 添加时的顺序。将全局中间件(如错误处理器、日志、CORS)放在特定于路由的中间件之前。
|
|
279
|
+
|
|
280
|
+
2. **错误处理器位置**:始终将错误处理中间件放在第一位,以确保它可以捕获所有后续中间件的错误。
|
|
281
|
+
|
|
282
|
+
3. **静态文件**:将静态文件服务中间件放在路由处理器之前,以提高性能(静态文件直接提供服务,无需经过路由处理)。
|
|
283
|
+
|
|
284
|
+
4. **验证范围**:将验证器中间件与特定路径和方法一起使用,以避免不必要的验证开销。
|
|
285
|
+
|
|
286
|
+
5. **环境特定配置**:为开发和生产环境配置不同的中间件(例如,仅在开发环境中暴露堆栈跟踪)。
|
|
287
|
+
|
|
288
|
+
## 许可证
|
|
289
|
+
|
|
290
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thinkbun/middleware",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"module": "src/index.ts",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"devDependencies": {
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"picocolors": "^1.1.1",
|
|
14
|
-
"@thinkbun/core": "1.0.
|
|
14
|
+
"@thinkbun/core": "1.0.6"
|
|
15
15
|
},
|
|
16
16
|
"publishConfig": {
|
|
17
17
|
"access": "public"
|
package/src/middlewares/cors.ts
CHANGED
|
@@ -38,6 +38,18 @@ export interface CorsOptions {
|
|
|
38
38
|
preflightContinue?: boolean;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
/**
|
|
42
|
+
* CORS中间件函数
|
|
43
|
+
*
|
|
44
|
+
* 处理跨域资源共享,支持:
|
|
45
|
+
* - 自定义允许的源
|
|
46
|
+
* - 自定义HTTP方法
|
|
47
|
+
* - 预检请求处理
|
|
48
|
+
* - 凭证支持
|
|
49
|
+
*
|
|
50
|
+
* @param options CORS配置选项
|
|
51
|
+
* @returns 返回一个中间件函数
|
|
52
|
+
*/
|
|
41
53
|
export function cors(options: CorsOptions = {}): Middleware {
|
|
42
54
|
const {
|
|
43
55
|
origin = '*',
|
|
@@ -52,7 +64,12 @@ export function cors(options: CorsOptions = {}): Middleware {
|
|
|
52
64
|
// 格式化methods为字符串
|
|
53
65
|
const methodsStr = methods.join(', ').toUpperCase();
|
|
54
66
|
|
|
55
|
-
|
|
67
|
+
/**
|
|
68
|
+
* 检查origin是否允许
|
|
69
|
+
* @private
|
|
70
|
+
* @param originHeader 请求头中的Origin值
|
|
71
|
+
* @returns 允许的origin字符串或false
|
|
72
|
+
*/
|
|
56
73
|
const checkOrigin = (originHeader: string | null): string | false => {
|
|
57
74
|
if (!originHeader) return false;
|
|
58
75
|
|
|
@@ -75,6 +92,34 @@ export function cors(options: CorsOptions = {}): Middleware {
|
|
|
75
92
|
return false;
|
|
76
93
|
};
|
|
77
94
|
|
|
95
|
+
/**
|
|
96
|
+
* 设置CORS响应头
|
|
97
|
+
* @private
|
|
98
|
+
* @param headers Headers对象
|
|
99
|
+
* @param allowedOrigin 允许的origin
|
|
100
|
+
* @param isPreflight 是否为预检请求
|
|
101
|
+
*/
|
|
102
|
+
const setCORSHeaders = (headers: Headers, allowedOrigin: string, isPreflight = false) => {
|
|
103
|
+
headers.set('Access-Control-Allow-Origin', allowedOrigin);
|
|
104
|
+
|
|
105
|
+
if (isPreflight) {
|
|
106
|
+
headers.set('Access-Control-Allow-Methods', methodsStr);
|
|
107
|
+
headers.set(
|
|
108
|
+
'Access-Control-Allow-Headers',
|
|
109
|
+
allowedHeaders?.join(', ') || headers.get('Access-Control-Request-Headers') || '',
|
|
110
|
+
);
|
|
111
|
+
headers.set('Access-Control-Max-Age', maxAge.toString());
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (credentials) {
|
|
115
|
+
headers.set('Access-Control-Allow-Credentials', 'true');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (exposedHeaders && exposedHeaders.length > 0 && !isPreflight) {
|
|
119
|
+
headers.set('Access-Control-Expose-Headers', exposedHeaders.join(', '));
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
78
123
|
return async (ctx: Context, next: () => Promise<Response | Context | void>): Promise<Response | Context | void> => {
|
|
79
124
|
// 处理OPTIONS预检请求
|
|
80
125
|
if (ctx.method === 'OPTIONS') {
|
|
@@ -87,16 +132,11 @@ export function cors(options: CorsOptions = {}): Middleware {
|
|
|
87
132
|
}
|
|
88
133
|
|
|
89
134
|
// 构建预检响应
|
|
90
|
-
const
|
|
135
|
+
const preflightHeaders = new Headers();
|
|
136
|
+
setCORSHeaders(preflightHeaders, allowedOrigin, true);
|
|
137
|
+
const preflightResponse = new Response(null, {
|
|
91
138
|
status: 204,
|
|
92
|
-
headers:
|
|
93
|
-
'Access-Control-Allow-Origin': allowedOrigin,
|
|
94
|
-
'Access-Control-Allow-Methods': methodsStr,
|
|
95
|
-
'Access-Control-Allow-Headers':
|
|
96
|
-
allowedHeaders?.join(', ') || ctx.header('Access-Control-Request-Headers') || '',
|
|
97
|
-
'Access-Control-Max-Age': maxAge.toString(),
|
|
98
|
-
...(credentials && { 'Access-Control-Allow-Credentials': 'true' }),
|
|
99
|
-
},
|
|
139
|
+
headers: preflightHeaders,
|
|
100
140
|
});
|
|
101
141
|
|
|
102
142
|
if (!preflightContinue) {
|
|
@@ -110,7 +150,7 @@ export function cors(options: CorsOptions = {}): Middleware {
|
|
|
110
150
|
if (nextResponse instanceof Response) {
|
|
111
151
|
const headers = new Headers(nextResponse.headers);
|
|
112
152
|
|
|
113
|
-
//
|
|
153
|
+
// 复制预检响应的所有头到新响应
|
|
114
154
|
preflightResponse.headers.forEach((value, name) => {
|
|
115
155
|
headers.set(name, value);
|
|
116
156
|
});
|
|
@@ -143,17 +183,9 @@ export function cors(options: CorsOptions = {}): Middleware {
|
|
|
143
183
|
const response = await next();
|
|
144
184
|
|
|
145
185
|
// 设置CORS响应头
|
|
146
|
-
const
|
|
186
|
+
const setCORSHeadersOnResponse = (res: Response) => {
|
|
147
187
|
const headers = new Headers(res.headers);
|
|
148
|
-
headers
|
|
149
|
-
|
|
150
|
-
if (credentials) {
|
|
151
|
-
headers.set('Access-Control-Allow-Credentials', 'true');
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
if (exposedHeaders && exposedHeaders.length > 0) {
|
|
155
|
-
headers.set('Access-Control-Expose-Headers', exposedHeaders.join(', '));
|
|
156
|
-
}
|
|
188
|
+
setCORSHeaders(headers, allowedOrigin);
|
|
157
189
|
|
|
158
190
|
return new Response(res.body, {
|
|
159
191
|
status: res.status,
|
|
@@ -163,18 +195,10 @@ export function cors(options: CorsOptions = {}): Middleware {
|
|
|
163
195
|
};
|
|
164
196
|
|
|
165
197
|
if (response instanceof Response) {
|
|
166
|
-
return
|
|
198
|
+
return setCORSHeadersOnResponse(response);
|
|
167
199
|
} else {
|
|
168
200
|
// 如果返回的是上下文对象或undefined,设置响应头
|
|
169
|
-
ctx.responseHeaders
|
|
170
|
-
|
|
171
|
-
if (credentials) {
|
|
172
|
-
ctx.responseHeaders.set('Access-Control-Allow-Credentials', 'true');
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
if (exposedHeaders && exposedHeaders.length > 0) {
|
|
176
|
-
ctx.responseHeaders.set('Access-Control-Expose-Headers', exposedHeaders.join(', '));
|
|
177
|
-
}
|
|
201
|
+
setCORSHeaders(ctx.responseHeaders, allowedOrigin);
|
|
178
202
|
}
|
|
179
203
|
|
|
180
204
|
return response;
|
|
@@ -21,6 +21,51 @@ export interface ErrorHandlerOptions {
|
|
|
21
21
|
logger?: (error: Error, ctx: Context) => void;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* 错误类型映射表
|
|
26
|
+
*/
|
|
27
|
+
const ERROR_TYPE_MAP: Record<string, { status: number; defaultMessage: string }> = {
|
|
28
|
+
BadRequestError: { status: 400, defaultMessage: 'Bad Request' },
|
|
29
|
+
UnauthorizedError: { status: 401, defaultMessage: 'Unauthorized' },
|
|
30
|
+
ForbiddenError: { status: 403, defaultMessage: 'Forbidden' },
|
|
31
|
+
NotFoundError: { status: 404, defaultMessage: 'Not Found' },
|
|
32
|
+
MethodNotAllowedError: { status: 405, defaultMessage: 'Method Not Allowed' },
|
|
33
|
+
ConflictError: { status: 409, defaultMessage: 'Conflict' },
|
|
34
|
+
UnprocessableEntityError: { status: 422, defaultMessage: 'Unprocessable Entity' },
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 根据错误类型获取状态码和消息
|
|
39
|
+
* @private
|
|
40
|
+
* @param error 错误对象
|
|
41
|
+
* @returns 包含状态码和消息的对象
|
|
42
|
+
*/
|
|
43
|
+
const getErrorInfo = (error: Error) => {
|
|
44
|
+
const errorType = ERROR_TYPE_MAP[error.name];
|
|
45
|
+
if (errorType) {
|
|
46
|
+
return {
|
|
47
|
+
status: errorType.status,
|
|
48
|
+
message: error.message || errorType.defaultMessage,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
status: 500,
|
|
53
|
+
message: 'Internal Server Error',
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 错误处理中间件函数
|
|
59
|
+
*
|
|
60
|
+
* 捕获和处理应用中的错误,支持:
|
|
61
|
+
* - 自定义错误处理函数
|
|
62
|
+
* - 错误日志记录
|
|
63
|
+
* - 根据错误类型设置HTTP状态码
|
|
64
|
+
* - 生产环境下的错误信息隐藏
|
|
65
|
+
*
|
|
66
|
+
* @param options 错误处理配置选项
|
|
67
|
+
* @returns 返回一个中间件函数
|
|
68
|
+
*/
|
|
24
69
|
export function errorHandler(options: ErrorHandlerOptions = {}): Middleware {
|
|
25
70
|
const { exposeStackTrace = process.env.NODE_ENV !== 'production', customHandler, logger = console.error } = options;
|
|
26
71
|
|
|
@@ -41,32 +86,7 @@ export function errorHandler(options: ErrorHandlerOptions = {}): Middleware {
|
|
|
41
86
|
|
|
42
87
|
// 默认错误处理
|
|
43
88
|
const err = error as Error;
|
|
44
|
-
|
|
45
|
-
let message = 'Internal Server Error';
|
|
46
|
-
|
|
47
|
-
// 根据错误类型设置状态码
|
|
48
|
-
if (err.name === 'BadRequestError') {
|
|
49
|
-
status = 400;
|
|
50
|
-
message = err.message || 'Bad Request';
|
|
51
|
-
} else if (err.name === 'UnauthorizedError') {
|
|
52
|
-
status = 401;
|
|
53
|
-
message = err.message || 'Unauthorized';
|
|
54
|
-
} else if (err.name === 'ForbiddenError') {
|
|
55
|
-
status = 403;
|
|
56
|
-
message = err.message || 'Forbidden';
|
|
57
|
-
} else if (err.name === 'NotFoundError') {
|
|
58
|
-
status = 404;
|
|
59
|
-
message = err.message || 'Not Found';
|
|
60
|
-
} else if (err.name === 'MethodNotAllowedError') {
|
|
61
|
-
status = 405;
|
|
62
|
-
message = err.message || 'Method Not Allowed';
|
|
63
|
-
} else if (err.name === 'ConflictError') {
|
|
64
|
-
status = 409;
|
|
65
|
-
message = err.message || 'Conflict';
|
|
66
|
-
} else if (err.name === 'UnprocessableEntityError') {
|
|
67
|
-
status = 422;
|
|
68
|
-
message = err.message || 'Unprocessable Entity';
|
|
69
|
-
}
|
|
89
|
+
const { status, message } = getErrorInfo(err);
|
|
70
90
|
|
|
71
91
|
// 构建错误响应
|
|
72
92
|
const errorResponse = {
|
|
@@ -29,6 +29,18 @@ export interface LoggerOptions {
|
|
|
29
29
|
logResponseBody?: boolean;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* 日志中间件函数
|
|
34
|
+
*
|
|
35
|
+
* 记录HTTP请求和响应信息,支持:
|
|
36
|
+
* - 文本和JSON两种日志格式
|
|
37
|
+
* - 自定义日志记录函数
|
|
38
|
+
* - 请求和响应体记录
|
|
39
|
+
* - 路径过滤
|
|
40
|
+
*
|
|
41
|
+
* @param options 日志配置选项
|
|
42
|
+
* @returns 返回一个中间件函数
|
|
43
|
+
*/
|
|
32
44
|
export function logger(options: LoggerOptions = {}): Middleware {
|
|
33
45
|
const {
|
|
34
46
|
format = 'text',
|
|
@@ -38,21 +50,39 @@ export function logger(options: LoggerOptions = {}): Middleware {
|
|
|
38
50
|
logResponseBody = false,
|
|
39
51
|
} = options;
|
|
40
52
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
53
|
+
/**
|
|
54
|
+
* 记录日志的通用方法
|
|
55
|
+
* @private
|
|
56
|
+
* @param message 日志消息
|
|
57
|
+
* @param data 日志数据对象
|
|
58
|
+
*/
|
|
59
|
+
const writeLog = (message: string, data?: Record<string, any>) => {
|
|
60
|
+
if (format === 'json') {
|
|
61
|
+
logger(JSON.stringify(data || { message }));
|
|
62
|
+
} else {
|
|
63
|
+
logger(message);
|
|
45
64
|
}
|
|
65
|
+
};
|
|
46
66
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
67
|
+
/**
|
|
68
|
+
* 创建请求信息对象
|
|
69
|
+
* @private
|
|
70
|
+
* @param ctx 上下文对象
|
|
71
|
+
* @param includeBody 是否包含请求体
|
|
72
|
+
* @returns 请求信息对象
|
|
73
|
+
*/
|
|
74
|
+
const createRequestInfo = async (
|
|
75
|
+
ctx: Context,
|
|
76
|
+
includeBody = false,
|
|
77
|
+
): Promise<{
|
|
78
|
+
method: string;
|
|
79
|
+
path: string;
|
|
80
|
+
ip: string;
|
|
81
|
+
userAgent: string;
|
|
82
|
+
query: Record<string, string>;
|
|
83
|
+
body?: unknown;
|
|
84
|
+
}> => {
|
|
85
|
+
const requestInfo = {
|
|
56
86
|
method: ctx.method,
|
|
57
87
|
path: ctx.path,
|
|
58
88
|
ip: ctx.ip?.address || 'unknown',
|
|
@@ -60,6 +90,28 @@ export function logger(options: LoggerOptions = {}): Middleware {
|
|
|
60
90
|
query: Object.fromEntries(ctx.url.searchParams),
|
|
61
91
|
};
|
|
62
92
|
|
|
93
|
+
// 记录请求体(如果需要)
|
|
94
|
+
if (includeBody && ctx.method !== 'GET') {
|
|
95
|
+
try {
|
|
96
|
+
const body = await ctx.body();
|
|
97
|
+
requestInfo.body = body;
|
|
98
|
+
} catch (_error) {
|
|
99
|
+
// 忽略读取请求体错误
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return requestInfo;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
return async (ctx: Context, next: () => Promise<Response | Context | void>): Promise<Response | Context | void> => {
|
|
107
|
+
// 检查是否需要忽略该路径的日志
|
|
108
|
+
if (ignorePaths.includes(ctx.path)) {
|
|
109
|
+
return await next();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const startTime = Date.now();
|
|
113
|
+
const requestInfo = await createRequestInfo(ctx, logRequestBody);
|
|
114
|
+
|
|
63
115
|
// 记录请求体(如果需要)
|
|
64
116
|
if (logRequestBody && ctx.method !== 'GET') {
|
|
65
117
|
try {
|
|
@@ -85,13 +137,10 @@ export function logger(options: LoggerOptions = {}): Middleware {
|
|
|
85
137
|
stack: (error as Error).stack,
|
|
86
138
|
};
|
|
87
139
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
`[ERROR] ${requestInfo.method} ${requestInfo.path} ${500} ${endTime - startTime}ms - ${(error as Error).message}`,
|
|
93
|
-
);
|
|
94
|
-
}
|
|
140
|
+
writeLog(
|
|
141
|
+
`[ERROR] ${requestInfo.method} ${requestInfo.path} 500 ${endTime - startTime}ms - ${(error as Error).message}`,
|
|
142
|
+
errorInfo,
|
|
143
|
+
);
|
|
95
144
|
|
|
96
145
|
throw error;
|
|
97
146
|
} finally {
|
|
@@ -132,11 +181,10 @@ export function logger(options: LoggerOptions = {}): Middleware {
|
|
|
132
181
|
}
|
|
133
182
|
}
|
|
134
183
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
184
|
+
writeLog(
|
|
185
|
+
`[INFO] ${requestInfo.method} ${requestInfo.path} ${response.status} ${endTime - startTime}ms`,
|
|
186
|
+
responseInfo,
|
|
187
|
+
);
|
|
140
188
|
}
|
|
141
189
|
}
|
|
142
190
|
};
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { describe, expect, it } from 'bun:test';
|
|
2
|
-
|
|
2
|
+
|
|
3
3
|
import type { Context } from '@thinkbun/core';
|
|
4
4
|
|
|
5
|
+
import { cors, errorHandler, logger, staticServe, validator } from '../index';
|
|
6
|
+
|
|
5
7
|
// 创建模拟上下文
|
|
6
8
|
function createMockContext(req: Request): Context {
|
|
7
9
|
const mockApp: any = {
|
|
8
10
|
server: {
|
|
9
|
-
requestIP: () => '127.0.0.1'
|
|
11
|
+
requestIP: () => '127.0.0.1',
|
|
10
12
|
},
|
|
11
|
-
logger: console
|
|
13
|
+
logger: console,
|
|
12
14
|
};
|
|
13
15
|
|
|
14
16
|
const ctx: any = {
|
|
@@ -51,13 +53,17 @@ function createMockContext(req: Request): Context {
|
|
|
51
53
|
clearCookie: () => ctx,
|
|
52
54
|
body: async () => ({}),
|
|
53
55
|
text: async () => '',
|
|
54
|
-
json: (data: any, options: any = {}) =>
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
json: (data: any, options: any = {}) =>
|
|
57
|
+
new Response(JSON.stringify(data), {
|
|
58
|
+
status: options.status || ctx.status,
|
|
59
|
+
headers: new Headers([
|
|
60
|
+
...ctx.responseHeaders.entries(),
|
|
61
|
+
...(options.headers ? Object.entries(options.headers) : []),
|
|
62
|
+
]),
|
|
63
|
+
}),
|
|
58
64
|
ok: () => new Response(null, { status: 200, headers: ctx.responseHeaders }),
|
|
59
65
|
success: (data: any) => ctx.json({ errno: 0, data }),
|
|
60
|
-
fail: (errno: number, msg: string) => ctx.json({ errno, msg })
|
|
66
|
+
fail: (errno: number, msg: string) => ctx.json({ errno, msg }),
|
|
61
67
|
};
|
|
62
68
|
|
|
63
69
|
return ctx;
|
|
@@ -69,89 +75,89 @@ describe('Middleware Tests', () => {
|
|
|
69
75
|
it('should add CORS headers to response', async () => {
|
|
70
76
|
const req = new Request('http://test:5300/', {
|
|
71
77
|
headers: {
|
|
72
|
-
Origin: 'http://example.com'
|
|
73
|
-
}
|
|
78
|
+
Origin: 'http://example.com',
|
|
79
|
+
},
|
|
74
80
|
});
|
|
75
81
|
const ctx = createMockContext(req);
|
|
76
82
|
const corsMiddleware = cors();
|
|
77
|
-
|
|
83
|
+
|
|
78
84
|
await corsMiddleware(ctx, async () => {});
|
|
79
|
-
|
|
85
|
+
|
|
80
86
|
expect(ctx.responseHeaders.get('Access-Control-Allow-Origin')).toBe('*'); // 默认配置下使用*
|
|
81
87
|
expect(ctx.responseHeaders.get('Access-Control-Allow-Methods')).toBeNull(); // 非OPTIONS请求不会添加这个头
|
|
82
88
|
expect(ctx.responseHeaders.get('Access-Control-Allow-Headers')).toBeNull(); // 非OPTIONS请求不会添加这个头
|
|
83
89
|
expect(ctx.responseHeaders.get('Access-Control-Max-Age')).toBeNull(); // 非OPTIONS请求不会添加这个头
|
|
84
90
|
});
|
|
85
|
-
|
|
91
|
+
|
|
86
92
|
it('should handle OPTIONS requests', async () => {
|
|
87
93
|
const req = new Request('http://test:5300/', {
|
|
88
94
|
method: 'OPTIONS',
|
|
89
95
|
headers: {
|
|
90
96
|
Origin: 'http://example.com',
|
|
91
97
|
'Access-Control-Request-Methods': 'GET, POST',
|
|
92
|
-
'Access-Control-Request-Headers': 'Content-Type'
|
|
93
|
-
}
|
|
98
|
+
'Access-Control-Request-Headers': 'Content-Type',
|
|
99
|
+
},
|
|
94
100
|
});
|
|
95
101
|
const ctx = createMockContext(req);
|
|
96
102
|
const corsMiddleware = cors();
|
|
97
|
-
|
|
103
|
+
|
|
98
104
|
const result = await corsMiddleware(ctx, async () => {});
|
|
99
|
-
|
|
105
|
+
|
|
100
106
|
expect(result).toBeDefined();
|
|
101
107
|
expect((result as Response).status).toBe(204);
|
|
102
108
|
});
|
|
103
109
|
});
|
|
104
|
-
|
|
110
|
+
|
|
105
111
|
// 测试Logger中间件
|
|
106
112
|
describe('Logger Middleware', () => {
|
|
107
113
|
it('should log request and response information', async () => {
|
|
108
114
|
const req = new Request('http://test:5300/');
|
|
109
115
|
const ctx = createMockContext(req);
|
|
110
|
-
|
|
116
|
+
|
|
111
117
|
let logCalled = false;
|
|
112
118
|
const loggerMiddleware = logger({
|
|
113
|
-
logger: () => logCalled = true
|
|
119
|
+
logger: () => (logCalled = true),
|
|
114
120
|
});
|
|
115
|
-
|
|
121
|
+
|
|
116
122
|
await loggerMiddleware(ctx, async () => {
|
|
117
123
|
return new Response('OK', { status: 200 });
|
|
118
124
|
});
|
|
119
|
-
|
|
125
|
+
|
|
120
126
|
expect(logCalled).toBe(true);
|
|
121
127
|
});
|
|
122
128
|
});
|
|
123
|
-
|
|
129
|
+
|
|
124
130
|
// 测试ErrorHandler中间件
|
|
125
131
|
describe('ErrorHandler Middleware', () => {
|
|
126
132
|
it('should handle errors and return JSON response', async () => {
|
|
127
133
|
const req = new Request('http://test:5300/');
|
|
128
134
|
const ctx = createMockContext(req);
|
|
129
135
|
const errorHandlerMiddleware = errorHandler();
|
|
130
|
-
|
|
136
|
+
|
|
131
137
|
const result = await errorHandlerMiddleware(ctx, async () => {
|
|
132
138
|
throw new Error('Test error');
|
|
133
139
|
});
|
|
134
|
-
|
|
140
|
+
|
|
135
141
|
expect(result).toBeDefined();
|
|
136
142
|
const response = result as Response;
|
|
137
143
|
expect(response.status).toBe(500);
|
|
138
144
|
const responseData = await response.json();
|
|
139
145
|
expect(responseData.error).toBe('Internal Server Error');
|
|
140
146
|
});
|
|
141
|
-
|
|
147
|
+
|
|
142
148
|
it('should handle HTTP errors with specific status codes', async () => {
|
|
143
149
|
const req = new Request('http://test:5300/');
|
|
144
150
|
const ctx = createMockContext(req);
|
|
145
151
|
const errorHandlerMiddleware = errorHandler();
|
|
146
|
-
|
|
152
|
+
|
|
147
153
|
// 使用与errorHandler中间件匹配的错误类型
|
|
148
154
|
const notFoundError = new Error('Not Found');
|
|
149
155
|
notFoundError.name = 'NotFoundError';
|
|
150
|
-
|
|
156
|
+
|
|
151
157
|
const result = await errorHandlerMiddleware(ctx, async () => {
|
|
152
158
|
throw notFoundError;
|
|
153
159
|
});
|
|
154
|
-
|
|
160
|
+
|
|
155
161
|
expect(result).toBeDefined();
|
|
156
162
|
const response = result as Response;
|
|
157
163
|
expect(response.status).toBe(404);
|
|
@@ -159,23 +165,23 @@ describe('Middleware Tests', () => {
|
|
|
159
165
|
expect(responseData.error).toBe('Not Found');
|
|
160
166
|
});
|
|
161
167
|
});
|
|
162
|
-
|
|
168
|
+
|
|
163
169
|
// 测试Validator中间件
|
|
164
170
|
describe('Validator Middleware', () => {
|
|
165
171
|
it('should validate request body against schema', async () => {
|
|
166
172
|
const req = new Request('http://test:5300/', {
|
|
167
173
|
method: 'POST',
|
|
168
174
|
headers: { 'Content-Type': 'application/json' },
|
|
169
|
-
body: JSON.stringify({ name: 'test', age: 25 })
|
|
175
|
+
body: JSON.stringify({ name: 'test', age: 25 }),
|
|
170
176
|
});
|
|
171
177
|
const ctx = createMockContext(req);
|
|
172
|
-
|
|
178
|
+
|
|
173
179
|
// Mock the body method
|
|
174
180
|
ctx.body = async () => ({ name: 'test', age: 25 });
|
|
175
|
-
|
|
181
|
+
|
|
176
182
|
// 设置params属性
|
|
177
183
|
ctx.params = {};
|
|
178
|
-
|
|
184
|
+
|
|
179
185
|
const validatorMiddleware = validator({
|
|
180
186
|
rules: [
|
|
181
187
|
{
|
|
@@ -186,37 +192,37 @@ describe('Middleware Tests', () => {
|
|
|
186
192
|
type: 'object',
|
|
187
193
|
properties: {
|
|
188
194
|
name: { type: 'string' },
|
|
189
|
-
age: { type: 'number' }
|
|
195
|
+
age: { type: 'number' },
|
|
190
196
|
},
|
|
191
|
-
required: ['name', 'age']
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
]
|
|
197
|
+
required: ['name', 'age'],
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
],
|
|
196
202
|
});
|
|
197
|
-
|
|
203
|
+
|
|
198
204
|
let nextCalled = false;
|
|
199
205
|
await validatorMiddleware(ctx, async () => {
|
|
200
206
|
nextCalled = true;
|
|
201
207
|
});
|
|
202
|
-
|
|
208
|
+
|
|
203
209
|
expect(nextCalled).toBe(true);
|
|
204
210
|
});
|
|
205
|
-
|
|
211
|
+
|
|
206
212
|
it('should throw error for invalid request body', async () => {
|
|
207
213
|
const req = new Request('http://test:5300/', {
|
|
208
214
|
method: 'POST',
|
|
209
215
|
headers: { 'Content-Type': 'application/json' },
|
|
210
|
-
body: JSON.stringify({ name: 'test' })
|
|
216
|
+
body: JSON.stringify({ name: 'test' }),
|
|
211
217
|
});
|
|
212
218
|
const ctx = createMockContext(req);
|
|
213
|
-
|
|
219
|
+
|
|
214
220
|
// Mock the body method
|
|
215
221
|
ctx.body = async () => ({ name: 'test' });
|
|
216
|
-
|
|
222
|
+
|
|
217
223
|
// 设置params属性
|
|
218
224
|
ctx.params = {};
|
|
219
|
-
|
|
225
|
+
|
|
220
226
|
const validatorMiddleware = validator({
|
|
221
227
|
rules: [
|
|
222
228
|
{
|
|
@@ -227,28 +233,28 @@ describe('Middleware Tests', () => {
|
|
|
227
233
|
type: 'object',
|
|
228
234
|
properties: {
|
|
229
235
|
name: { type: 'string' },
|
|
230
|
-
age: { type: 'number' }
|
|
236
|
+
age: { type: 'number' },
|
|
231
237
|
},
|
|
232
|
-
required: ['name', 'age']
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
]
|
|
238
|
+
required: ['name', 'age'],
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
],
|
|
237
243
|
});
|
|
238
|
-
|
|
244
|
+
|
|
239
245
|
await expect(validatorMiddleware(ctx, async () => {})).rejects.toThrow();
|
|
240
246
|
});
|
|
241
247
|
});
|
|
242
|
-
|
|
248
|
+
|
|
243
249
|
// 测试StaticServe中间件
|
|
244
250
|
describe('StaticServe Middleware', () => {
|
|
245
251
|
it('should serve static files', async () => {
|
|
246
252
|
const req = new Request('http://test:5300/test.txt');
|
|
247
253
|
const ctx = createMockContext(req);
|
|
248
|
-
|
|
254
|
+
|
|
249
255
|
// 这个测试会实际查找文件,所以我们只测试中间件是否能正常执行
|
|
250
256
|
const staticMiddleware = staticServe({ root: './public' });
|
|
251
|
-
|
|
257
|
+
|
|
252
258
|
let nextCalled = false;
|
|
253
259
|
try {
|
|
254
260
|
await staticMiddleware(ctx, async () => {
|
|
@@ -257,7 +263,7 @@ describe('Middleware Tests', () => {
|
|
|
257
263
|
} catch (error) {
|
|
258
264
|
// 忽略文件不存在的错误
|
|
259
265
|
}
|
|
260
|
-
|
|
266
|
+
|
|
261
267
|
// 因为文件可能不存在,所以无论如何都应该调用next
|
|
262
268
|
expect(nextCalled).toBe(true);
|
|
263
269
|
});
|