@wooksjs/event-http 0.6.6 → 0.7.1
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 +11 -45
- package/dist/index.cjs +1148 -968
- package/dist/index.d.ts +364 -308
- package/dist/index.mjs +1104 -920
- package/package.json +7 -7
- package/skills/wooksjs-event-http/SKILL.md +28 -21
- package/skills/wooksjs-event-http/core.md +83 -228
- package/skills/wooksjs-event-http/request.md +131 -147
- package/skills/wooksjs-event-http/response.md +166 -235
- package/skills/wooksjs-event-http/testing.md +150 -0
- package/skills/wooksjs-event-http/addons.md +0 -307
- package/skills/wooksjs-event-http/error-handling.md +0 -253
- package/skills/wooksjs-event-http/event-core.md +0 -562
- package/skills/wooksjs-event-http/routing.md +0 -412
|
@@ -1,307 +0,0 @@
|
|
|
1
|
-
# Addons — Body Parser, Static Files, Proxy
|
|
2
|
-
|
|
3
|
-
> Covers the three official addon packages: `@wooksjs/http-body` for parsing request bodies, `@wooksjs/http-static` for serving static files, and `@wooksjs/http-proxy` for proxying requests.
|
|
4
|
-
|
|
5
|
-
## Body Parser — `@wooksjs/http-body`
|
|
6
|
-
|
|
7
|
-
### Installation
|
|
8
|
-
|
|
9
|
-
```bash
|
|
10
|
-
npm install @wooksjs/http-body
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
### `useBody()`
|
|
14
|
-
|
|
15
|
-
Composable that provides content-type detection and body parsing:
|
|
16
|
-
|
|
17
|
-
```ts
|
|
18
|
-
import { useBody } from '@wooksjs/http-body'
|
|
19
|
-
|
|
20
|
-
app.post('/api/data', async () => {
|
|
21
|
-
const { parseBody, isJson, isFormData } = useBody()
|
|
22
|
-
|
|
23
|
-
if (isJson()) {
|
|
24
|
-
const data = await parseBody<{ name: string; age: number }>()
|
|
25
|
-
return { received: data }
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
if (isFormData()) {
|
|
29
|
-
const formData = await parseBody<Record<string, string>>()
|
|
30
|
-
return { fields: formData }
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
return { error: 'Unsupported content type' }
|
|
34
|
-
})
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
### Content-type checkers
|
|
38
|
-
|
|
39
|
-
All checkers are lazy-computed and cached:
|
|
40
|
-
|
|
41
|
-
| Method | Checks for |
|
|
42
|
-
|--------|-----------|
|
|
43
|
-
| `isJson()` | `application/json` |
|
|
44
|
-
| `isHtml()` | `text/html` |
|
|
45
|
-
| `isXml()` | `text/xml` |
|
|
46
|
-
| `isText()` | `text/plain` |
|
|
47
|
-
| `isBinary()` | `application/octet-stream` |
|
|
48
|
-
| `isFormData()` | `multipart/form-data` |
|
|
49
|
-
| `isUrlencoded()` | `application/x-www-form-urlencoded` |
|
|
50
|
-
|
|
51
|
-
### `parseBody<T>()`
|
|
52
|
-
|
|
53
|
-
Parses the request body based on content type. Returns a promise. The result is cached — calling `parseBody()` twice returns the same promise.
|
|
54
|
-
|
|
55
|
-
| Content-Type | Parsed as |
|
|
56
|
-
|-------------|-----------|
|
|
57
|
-
| `application/json` | Parsed JSON object (safe parse with prototype pollution protection) |
|
|
58
|
-
| `multipart/form-data` | Parsed form fields as `Record<string, unknown>` |
|
|
59
|
-
| `application/x-www-form-urlencoded` | Parsed as object using `WooksURLSearchParams` |
|
|
60
|
-
| everything else | Raw text string |
|
|
61
|
-
|
|
62
|
-
### `rawBody()`
|
|
63
|
-
|
|
64
|
-
Also re-exports `rawBody()` from `useRequest()` for convenience:
|
|
65
|
-
|
|
66
|
-
```ts
|
|
67
|
-
const { rawBody } = useBody()
|
|
68
|
-
const buffer = await rawBody() // Buffer (decompressed if needed)
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
### Common Pattern: JSON API endpoint
|
|
72
|
-
|
|
73
|
-
```ts
|
|
74
|
-
app.post('/api/users', async () => {
|
|
75
|
-
const { parseBody, isJson } = useBody()
|
|
76
|
-
if (!isJson()) throw new HttpError(415, 'Expected JSON')
|
|
77
|
-
|
|
78
|
-
const body = await parseBody<{ name: string; email: string }>()
|
|
79
|
-
const user = await db.createUser(body)
|
|
80
|
-
|
|
81
|
-
const { status } = useResponse()
|
|
82
|
-
status(201)
|
|
83
|
-
return user
|
|
84
|
-
})
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
### Safety
|
|
88
|
-
|
|
89
|
-
- JSON parsing uses `safeJsonParse` which protects against `__proto__` and `constructor` pollution.
|
|
90
|
-
- Form-data parsing limits: max 255 fields, max 100-byte key names, max 100 KB per field value.
|
|
91
|
-
- Illegal keys (`__proto__`, `constructor`, `prototype`) are rejected with 400.
|
|
92
|
-
|
|
93
|
-
---
|
|
94
|
-
|
|
95
|
-
## Static Files — `@wooksjs/http-static`
|
|
96
|
-
|
|
97
|
-
### Installation
|
|
98
|
-
|
|
99
|
-
```bash
|
|
100
|
-
npm install @wooksjs/http-static
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
### `serveFile(filePath, options?)`
|
|
104
|
-
|
|
105
|
-
Serves a static file with full HTTP caching support (ETags, If-None-Match, If-Modified-Since, Range requests):
|
|
106
|
-
|
|
107
|
-
```ts
|
|
108
|
-
import { serveFile } from '@wooksjs/http-static'
|
|
109
|
-
|
|
110
|
-
app.get('/static/*', () => {
|
|
111
|
-
const { get } = useRouteParams<{ '*': string }>()
|
|
112
|
-
return serveFile(get('*'), {
|
|
113
|
-
baseDir: './public',
|
|
114
|
-
cacheControl: { maxAge: '1h', public: true },
|
|
115
|
-
})
|
|
116
|
-
})
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
### Options
|
|
120
|
-
|
|
121
|
-
```ts
|
|
122
|
-
interface TServeFileOptions {
|
|
123
|
-
baseDir?: string // base directory for file resolution
|
|
124
|
-
headers?: Record<string, string> // additional response headers
|
|
125
|
-
cacheControl?: TCacheControl // Cache-Control directives
|
|
126
|
-
expires?: Date | string | number // Expires header
|
|
127
|
-
pragmaNoCache?: boolean // set Pragma: no-cache
|
|
128
|
-
defaultExt?: string // default extension (e.g. 'html')
|
|
129
|
-
index?: string // index file (e.g. 'index.html')
|
|
130
|
-
listDirectory?: boolean // serve directory listing
|
|
131
|
-
allowDotDot?: boolean // allow ../ traversal (default: false)
|
|
132
|
-
}
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
### Features
|
|
136
|
-
|
|
137
|
-
- **ETag & conditional requests**: Automatically generates ETags from inode/size/mtime. Returns 304 Not Modified when appropriate.
|
|
138
|
-
- **Range requests**: Supports `Range` header for partial content (206) and `If-Range` validation.
|
|
139
|
-
- **MIME type detection**: Automatically sets `Content-Type` based on file extension.
|
|
140
|
-
- **Directory listing**: Optional HTML directory listing with file sizes and dates.
|
|
141
|
-
- **Index files**: Serves `index.html` (or configured index) when a directory is requested.
|
|
142
|
-
- **Path traversal protection**: Rejects `../` by default.
|
|
143
|
-
|
|
144
|
-
### Common Pattern: SPA with fallback
|
|
145
|
-
|
|
146
|
-
```ts
|
|
147
|
-
app.get('/*', async () => {
|
|
148
|
-
const { get } = useRouteParams<{ '*': string }>()
|
|
149
|
-
try {
|
|
150
|
-
return await serveFile(get('*') || 'index.html', {
|
|
151
|
-
baseDir: './dist',
|
|
152
|
-
index: 'index.html',
|
|
153
|
-
cacheControl: { maxAge: '1d', public: true },
|
|
154
|
-
})
|
|
155
|
-
} catch {
|
|
156
|
-
// Fallback to index.html for SPA client-side routing
|
|
157
|
-
return await serveFile('index.html', { baseDir: './dist' })
|
|
158
|
-
}
|
|
159
|
-
})
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
### Common Pattern: File server with directory listing
|
|
163
|
-
|
|
164
|
-
```ts
|
|
165
|
-
app.get('/files/*', () => {
|
|
166
|
-
const { get } = useRouteParams<{ '*': string }>()
|
|
167
|
-
return serveFile(get('*') || '.', {
|
|
168
|
-
baseDir: '/var/shared',
|
|
169
|
-
listDirectory: true,
|
|
170
|
-
cacheControl: { noCache: true },
|
|
171
|
-
})
|
|
172
|
-
})
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
---
|
|
176
|
-
|
|
177
|
-
## HTTP Proxy — `@wooksjs/http-proxy`
|
|
178
|
-
|
|
179
|
-
### Installation
|
|
180
|
-
|
|
181
|
-
```bash
|
|
182
|
-
npm install @wooksjs/http-proxy
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
### `useProxy()`
|
|
186
|
-
|
|
187
|
-
Composable that returns a `proxy()` function for forwarding the current request:
|
|
188
|
-
|
|
189
|
-
```ts
|
|
190
|
-
import { useProxy } from '@wooksjs/http-proxy'
|
|
191
|
-
|
|
192
|
-
app.get('/api/*', async () => {
|
|
193
|
-
const proxy = useProxy()
|
|
194
|
-
return await proxy('https://backend.example.com/api/endpoint', {
|
|
195
|
-
reqHeaders: { allow: '*' },
|
|
196
|
-
resHeaders: { allow: '*' },
|
|
197
|
-
})
|
|
198
|
-
})
|
|
199
|
-
```
|
|
200
|
-
|
|
201
|
-
### `proxy(target, options?)`
|
|
202
|
-
|
|
203
|
-
The function returned by `useProxy()`:
|
|
204
|
-
|
|
205
|
-
```ts
|
|
206
|
-
async function proxy(target: string, opts?: TWooksProxyOptions): Promise<Response>
|
|
207
|
-
```
|
|
208
|
-
|
|
209
|
-
- `target` — Full URL to proxy to (including path and query string).
|
|
210
|
-
- Returns a `Response` (Fetch API) that the framework sends as the final response.
|
|
211
|
-
|
|
212
|
-
### Proxy Options
|
|
213
|
-
|
|
214
|
-
```ts
|
|
215
|
-
interface TWooksProxyOptions {
|
|
216
|
-
method?: string // override HTTP method
|
|
217
|
-
reqHeaders?: TWooksProxyControls // filter/transform request headers
|
|
218
|
-
reqCookies?: TWooksProxyControls // filter/transform request cookies
|
|
219
|
-
resHeaders?: TWooksProxyControls // filter/transform response headers
|
|
220
|
-
resCookies?: TWooksProxyControls // filter/transform response cookies
|
|
221
|
-
debug?: boolean // log proxy details
|
|
222
|
-
}
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
### Proxy Controls
|
|
226
|
-
|
|
227
|
-
Each control object (`TWooksProxyControls`) lets you allow, block, or overwrite headers/cookies:
|
|
228
|
-
|
|
229
|
-
```ts
|
|
230
|
-
interface TWooksProxyControls {
|
|
231
|
-
allow?: Array<string | RegExp> | '*' // allowlist (use '*' for all)
|
|
232
|
-
block?: Array<string | RegExp> | '*' // blocklist (use '*' for none)
|
|
233
|
-
overwrite?: Record<string, string> | ((data: Record<string, string>) => Record<string, string>)
|
|
234
|
-
}
|
|
235
|
-
```
|
|
236
|
-
|
|
237
|
-
### Common Pattern: API Gateway
|
|
238
|
-
|
|
239
|
-
```ts
|
|
240
|
-
app.all('/api/*', async () => {
|
|
241
|
-
const proxy = useProxy()
|
|
242
|
-
const { get } = useRouteParams<{ '*': string }>()
|
|
243
|
-
const { rawSearchParams } = useSearchParams()
|
|
244
|
-
|
|
245
|
-
const targetPath = get('*')
|
|
246
|
-
const query = rawSearchParams()
|
|
247
|
-
const target = `https://internal-api.local/${targetPath}${query}`
|
|
248
|
-
|
|
249
|
-
return await proxy(target, {
|
|
250
|
-
reqHeaders: {
|
|
251
|
-
allow: '*',
|
|
252
|
-
overwrite: { 'x-gateway': 'wooks' },
|
|
253
|
-
},
|
|
254
|
-
resHeaders: { allow: '*' },
|
|
255
|
-
reqCookies: { allow: ['session_id'] },
|
|
256
|
-
resCookies: { allow: ['session_id'] },
|
|
257
|
-
})
|
|
258
|
-
})
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
### Common Pattern: Selective header forwarding
|
|
262
|
-
|
|
263
|
-
```ts
|
|
264
|
-
return await proxy('https://api.example.com/data', {
|
|
265
|
-
reqHeaders: {
|
|
266
|
-
allow: ['authorization', 'content-type', 'accept'],
|
|
267
|
-
},
|
|
268
|
-
resHeaders: {
|
|
269
|
-
allow: '*',
|
|
270
|
-
block: ['x-internal-header', 'server'],
|
|
271
|
-
},
|
|
272
|
-
})
|
|
273
|
-
```
|
|
274
|
-
|
|
275
|
-
### Blocked headers by default
|
|
276
|
-
|
|
277
|
-
When using proxy controls, these headers are blocked by default:
|
|
278
|
-
|
|
279
|
-
**Request**: `connection`, `accept-encoding`, `content-length`, `upgrade-insecure-requests`, `cookie`
|
|
280
|
-
|
|
281
|
-
**Response**: `transfer-encoding`, `content-encoding`, `set-cookie`
|
|
282
|
-
|
|
283
|
-
To forward cookies, explicitly configure `reqCookies` / `resCookies`.
|
|
284
|
-
|
|
285
|
-
### Debug mode
|
|
286
|
-
|
|
287
|
-
```ts
|
|
288
|
-
return await proxy('https://api.example.com/data', {
|
|
289
|
-
debug: true, // logs request/response details to the event logger
|
|
290
|
-
reqHeaders: { allow: '*' },
|
|
291
|
-
resHeaders: { allow: '*' },
|
|
292
|
-
})
|
|
293
|
-
```
|
|
294
|
-
|
|
295
|
-
## Best Practices
|
|
296
|
-
|
|
297
|
-
- **Always validate and sanitize paths before `serveFile()`** — Don't pass user input directly. Use `baseDir` to restrict the file root.
|
|
298
|
-
- **Use `parseBody()` only once** — It reads and caches the body. Multiple calls return the same cached result.
|
|
299
|
-
- **Use proxy controls to limit header forwarding** — Don't blindly forward all headers between services. Use `allow`/`block` to be explicit.
|
|
300
|
-
- **Set `allow: '*'` explicitly** — Without controls, no headers are forwarded by the proxy.
|
|
301
|
-
|
|
302
|
-
## Gotchas
|
|
303
|
-
|
|
304
|
-
- `serveFile()` throws `HttpError(404)` if the file doesn't exist. Catch it if you need fallback behavior.
|
|
305
|
-
- `serveFile()` rejects paths containing `/../` by default. Set `allowDotDot: true` only if you're certain the path is safe.
|
|
306
|
-
- `useProxy()` uses the global `fetch()` API — ensure your Node.js version supports it (18+).
|
|
307
|
-
- The proxy function returns a `Response` object. The framework handles streaming it to the client.
|
|
@@ -1,253 +0,0 @@
|
|
|
1
|
-
# Error Handling — @wooksjs/event-http
|
|
2
|
-
|
|
3
|
-
> Covers throwing HTTP errors, custom error bodies, error rendering, and error flow.
|
|
4
|
-
|
|
5
|
-
## Concepts
|
|
6
|
-
|
|
7
|
-
In Wooks, errors are raised by throwing an `HttpError` instance. The framework catches it and renders an appropriate error response based on the client's `Accept` header (JSON, HTML, or plain text).
|
|
8
|
-
|
|
9
|
-
Any uncaught `Error` thrown in a handler is automatically wrapped as a 500 Internal Server Error.
|
|
10
|
-
|
|
11
|
-
## `HttpError`
|
|
12
|
-
|
|
13
|
-
```ts
|
|
14
|
-
import { HttpError } from '@wooksjs/event-http'
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
### Basic usage
|
|
18
|
-
|
|
19
|
-
```ts
|
|
20
|
-
app.get('/users/:id', async () => {
|
|
21
|
-
const user = await db.findUser(id)
|
|
22
|
-
if (!user) {
|
|
23
|
-
throw new HttpError(404, 'User not found')
|
|
24
|
-
}
|
|
25
|
-
return user
|
|
26
|
-
})
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
This produces a response like:
|
|
30
|
-
|
|
31
|
-
```json
|
|
32
|
-
{ "statusCode": 404, "error": "Not Found", "message": "User not found" }
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
### Status code only
|
|
36
|
-
|
|
37
|
-
```ts
|
|
38
|
-
throw new HttpError(403)
|
|
39
|
-
// → { "statusCode": 403, "error": "Forbidden", "message": "" }
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
The `error` field is automatically populated from the standard HTTP status text.
|
|
43
|
-
|
|
44
|
-
### Custom error body
|
|
45
|
-
|
|
46
|
-
Pass an object as the second argument for additional fields:
|
|
47
|
-
|
|
48
|
-
```ts
|
|
49
|
-
throw new HttpError(422, {
|
|
50
|
-
message: 'Validation failed',
|
|
51
|
-
statusCode: 422,
|
|
52
|
-
fields: {
|
|
53
|
-
email: 'Invalid email format',
|
|
54
|
-
age: 'Must be positive',
|
|
55
|
-
},
|
|
56
|
-
})
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
Response:
|
|
60
|
-
|
|
61
|
-
```json
|
|
62
|
-
{
|
|
63
|
-
"statusCode": 422,
|
|
64
|
-
"error": "Unprocessable Entity",
|
|
65
|
-
"message": "Validation failed",
|
|
66
|
-
"fields": {
|
|
67
|
-
"email": "Invalid email format",
|
|
68
|
-
"age": "Must be positive"
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
### Constructor signature
|
|
74
|
-
|
|
75
|
-
```ts
|
|
76
|
-
class HttpError<T extends TWooksErrorBody = TWooksErrorBody> extends Error {
|
|
77
|
-
constructor(
|
|
78
|
-
code: THttpErrorCodes = 500, // HTTP status code (4xx, 5xx)
|
|
79
|
-
body: string | T = '', // message string or structured body
|
|
80
|
-
)
|
|
81
|
-
|
|
82
|
-
get body(): TWooksErrorBodyExt // always returns { statusCode, message, error, ...extra }
|
|
83
|
-
}
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
### Error body shape
|
|
87
|
-
|
|
88
|
-
```ts
|
|
89
|
-
interface TWooksErrorBody {
|
|
90
|
-
message: string
|
|
91
|
-
statusCode: EHttpStatusCode
|
|
92
|
-
error?: string
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Extended (always has error string)
|
|
96
|
-
interface TWooksErrorBodyExt extends TWooksErrorBody {
|
|
97
|
-
error: string // e.g. 'Not Found', 'Internal Server Error'
|
|
98
|
-
}
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
## Error Flow
|
|
102
|
-
|
|
103
|
-
1. Handler throws `HttpError` → framework catches it
|
|
104
|
-
2. Error body is constructed via `httpError.body` getter
|
|
105
|
-
3. `HttpErrorRenderer` checks the `Accept` header:
|
|
106
|
-
- `application/json` → JSON response
|
|
107
|
-
- `text/html` → styled HTML error page
|
|
108
|
-
- `text/plain` → plain text
|
|
109
|
-
- fallback → JSON
|
|
110
|
-
4. Status code from the error is set on the response
|
|
111
|
-
|
|
112
|
-
### Uncaught errors
|
|
113
|
-
|
|
114
|
-
Any non-`HttpError` thrown in a handler is wrapped as a 500:
|
|
115
|
-
|
|
116
|
-
```ts
|
|
117
|
-
app.get('/crash', () => {
|
|
118
|
-
throw new Error('something broke')
|
|
119
|
-
// → 500 Internal Server Error: "something broke"
|
|
120
|
-
})
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
### Error in handler chain
|
|
124
|
-
|
|
125
|
-
If multiple handlers are registered for a route, an error in a non-last handler falls through to the next handler. Only the last handler's error is sent as the response:
|
|
126
|
-
|
|
127
|
-
```ts
|
|
128
|
-
// This is the internal behavior — typically you register one handler per route
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
## Common Patterns
|
|
132
|
-
|
|
133
|
-
### Pattern: Guard function
|
|
134
|
-
|
|
135
|
-
```ts
|
|
136
|
-
function requireAuth() {
|
|
137
|
-
const { isBearer, authRawCredentials } = useAuthorization()
|
|
138
|
-
if (!isBearer()) {
|
|
139
|
-
throw new HttpError(401, 'Authentication required')
|
|
140
|
-
}
|
|
141
|
-
const token = authRawCredentials()!
|
|
142
|
-
const user = verifyToken(token)
|
|
143
|
-
if (!user) {
|
|
144
|
-
throw new HttpError(401, 'Invalid token')
|
|
145
|
-
}
|
|
146
|
-
return user
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
app.get('/protected', () => {
|
|
150
|
-
const user = requireAuth()
|
|
151
|
-
return { message: `Hello ${user.name}` }
|
|
152
|
-
})
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
### Pattern: Validation with details
|
|
156
|
-
|
|
157
|
-
```ts
|
|
158
|
-
function validateBody(data: unknown): asserts data is CreateUserDTO {
|
|
159
|
-
const errors: Record<string, string> = {}
|
|
160
|
-
|
|
161
|
-
if (!data || typeof data !== 'object') {
|
|
162
|
-
throw new HttpError(400, 'Request body must be an object')
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
const body = data as Record<string, unknown>
|
|
166
|
-
if (!body.email) errors.email = 'Required'
|
|
167
|
-
if (!body.name) errors.name = 'Required'
|
|
168
|
-
|
|
169
|
-
if (Object.keys(errors).length > 0) {
|
|
170
|
-
throw new HttpError(422, {
|
|
171
|
-
message: 'Validation failed',
|
|
172
|
-
statusCode: 422,
|
|
173
|
-
fields: errors,
|
|
174
|
-
})
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
### Pattern: Not Found with context
|
|
180
|
-
|
|
181
|
-
```ts
|
|
182
|
-
app.get('/users/:id', async () => {
|
|
183
|
-
const { get } = useRouteParams<{ id: string }>()
|
|
184
|
-
const id = get('id')
|
|
185
|
-
const user = await db.findUser(id)
|
|
186
|
-
if (!user) {
|
|
187
|
-
throw new HttpError(404, `User with id "${id}" not found`)
|
|
188
|
-
}
|
|
189
|
-
return user
|
|
190
|
-
})
|
|
191
|
-
```
|
|
192
|
-
|
|
193
|
-
### Pattern: Custom 404 handler
|
|
194
|
-
|
|
195
|
-
```ts
|
|
196
|
-
const app = createHttpApp({
|
|
197
|
-
onNotFound: () => {
|
|
198
|
-
const { url } = useRequest()
|
|
199
|
-
throw new HttpError(404, `Route ${url} does not exist`)
|
|
200
|
-
},
|
|
201
|
-
})
|
|
202
|
-
```
|
|
203
|
-
|
|
204
|
-
## Available Status Codes
|
|
205
|
-
|
|
206
|
-
`HttpError` accepts any valid HTTP error status code. Common ones:
|
|
207
|
-
|
|
208
|
-
| Code | Meaning |
|
|
209
|
-
|------|---------|
|
|
210
|
-
| 400 | Bad Request |
|
|
211
|
-
| 401 | Unauthorized |
|
|
212
|
-
| 403 | Forbidden |
|
|
213
|
-
| 404 | Not Found |
|
|
214
|
-
| 405 | Method Not Allowed |
|
|
215
|
-
| 408 | Request Timeout |
|
|
216
|
-
| 409 | Conflict |
|
|
217
|
-
| 413 | Payload Too Large |
|
|
218
|
-
| 415 | Unsupported Media Type |
|
|
219
|
-
| 416 | Range Not Satisfiable |
|
|
220
|
-
| 422 | Unprocessable Entity |
|
|
221
|
-
| 429 | Too Many Requests |
|
|
222
|
-
| 500 | Internal Server Error |
|
|
223
|
-
| 502 | Bad Gateway |
|
|
224
|
-
| 503 | Service Unavailable |
|
|
225
|
-
|
|
226
|
-
The `EHttpStatusCode` enum from `@wooksjs/event-http` provides all standard codes.
|
|
227
|
-
|
|
228
|
-
## Built-in Error Responses
|
|
229
|
-
|
|
230
|
-
The framework automatically throws `HttpError` in certain situations:
|
|
231
|
-
|
|
232
|
-
| Situation | Code | Message |
|
|
233
|
-
|-----------|------|---------|
|
|
234
|
-
| No route matched | 404 | (empty) |
|
|
235
|
-
| Body too large (compressed) | 413 | Payload Too Large |
|
|
236
|
-
| Body too large (inflated) | 413 | Inflated body too large |
|
|
237
|
-
| Compression ratio too high | 413 | Compression ratio too high |
|
|
238
|
-
| Unsupported Content-Encoding | 415 | Unsupported Content-Encoding "..." |
|
|
239
|
-
| Body read timeout | 408 | Request body timeout |
|
|
240
|
-
| Malformed JSON body | 400 | (parse error message) |
|
|
241
|
-
| Missing form-data boundary | 400 | form-data boundary not recognized |
|
|
242
|
-
|
|
243
|
-
## Best Practices
|
|
244
|
-
|
|
245
|
-
- **Throw `HttpError`, don't return error objects** — The framework detects `HttpError` specially and renders it with correct status and content negotiation.
|
|
246
|
-
- **Use meaningful status codes** — 400 for bad input, 401 for missing auth, 403 for insufficient permissions, 404 for not found, 422 for validation errors.
|
|
247
|
-
- **Include context in error messages** — `"User with id 42 not found"` is more useful than `"Not found"`.
|
|
248
|
-
- **Use guard functions** — Extract auth/validation into reusable functions that throw on failure.
|
|
249
|
-
|
|
250
|
-
## Gotchas
|
|
251
|
-
|
|
252
|
-
- Throwing a plain `Error` (not `HttpError`) results in a 500 with the error's message exposed. In production, you may want to catch and wrap errors to avoid leaking internals.
|
|
253
|
-
- `HttpError.body` always includes an `error` field with the standard HTTP status text, even if you didn't provide one.
|