@wooksjs/event-http 0.6.1 → 0.6.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.
- package/README.md +24 -0
- package/dist/index.cjs +510 -34
- package/dist/index.d.ts +134 -12
- package/dist/index.mjs +510 -34
- package/package.json +45 -37
- package/scripts/setup-skills.js +70 -0
- package/skills/wooksjs-event-http/SKILL.md +37 -0
- package/skills/wooksjs-event-http/addons.md +307 -0
- package/skills/wooksjs-event-http/core.md +297 -0
- package/skills/wooksjs-event-http/error-handling.md +253 -0
- package/skills/wooksjs-event-http/event-core.md +562 -0
- package/skills/wooksjs-event-http/request.md +220 -0
- package/skills/wooksjs-event-http/response.md +336 -0
- package/skills/wooksjs-event-http/routing.md +412 -0
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
# Core Concepts — @wooksjs/event-http
|
|
2
|
+
|
|
3
|
+
> Covers HTTP app creation, server setup, how the HTTP adapter integrates with the event context system, testing, and logging.
|
|
4
|
+
|
|
5
|
+
For the underlying event context store API (`init`, `get`, `set`, `hook`, etc.) and how to create custom composables, see [event-core.md](event-core.md).
|
|
6
|
+
|
|
7
|
+
## Mental Model
|
|
8
|
+
|
|
9
|
+
`@wooksjs/event-http` is the HTTP adapter for Wooks. It turns every incoming HTTP request into an event with its own isolated context store. Instead of middleware chains and mutated `req`/`res` objects, you call composable functions (`useRequest()`, `useCookies()`, `useSetHeaders()`, etc.) from anywhere in your handler — values are computed on demand and cached per request.
|
|
10
|
+
|
|
11
|
+
Key principles:
|
|
12
|
+
1. **Never mutate `req`** — Accumulate request context in the store instead.
|
|
13
|
+
2. **Never parse before needed** — Cookies, body, search params are only parsed when a composable is first called.
|
|
14
|
+
3. **No middleware sprawl** — Composable functions replace middleware. Each one is a focused, importable utility.
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install wooks @wooksjs/event-http
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Creating an HTTP App
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { createHttpApp } from '@wooksjs/event-http'
|
|
26
|
+
|
|
27
|
+
const app = createHttpApp()
|
|
28
|
+
|
|
29
|
+
app.get('/hello', () => 'Hello World!')
|
|
30
|
+
|
|
31
|
+
app.listen(3000, () => {
|
|
32
|
+
console.log('Server running on port 3000')
|
|
33
|
+
})
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`createHttpApp(opts?, wooks?)` returns a `WooksHttp` instance. Options:
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
interface TWooksHttpOptions {
|
|
40
|
+
logger?: TConsoleBase // custom logger
|
|
41
|
+
eventOptions?: TEventOptions // event-level logger config
|
|
42
|
+
onNotFound?: TWooksHandler // custom 404 handler
|
|
43
|
+
router?: {
|
|
44
|
+
ignoreTrailingSlash?: boolean // treat /path and /path/ as the same
|
|
45
|
+
ignoreCase?: boolean // case-insensitive matching
|
|
46
|
+
cacheLimit?: number // max cached parsed routes
|
|
47
|
+
}
|
|
48
|
+
requestLimits?: { // app-level body limits (overridable per-request)
|
|
49
|
+
maxCompressed?: number // default: 1 MB
|
|
50
|
+
maxInflated?: number // default: 10 MB
|
|
51
|
+
maxRatio?: number // default: 100 (zip-bomb protection)
|
|
52
|
+
readTimeoutMs?: number // default: 10 000 ms
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Example — raise body limits for the entire app:
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
const app = createHttpApp({
|
|
61
|
+
requestLimits: {
|
|
62
|
+
maxCompressed: 50 * 1024 * 1024, // 50 MB
|
|
63
|
+
maxInflated: 100 * 1024 * 1024, // 100 MB
|
|
64
|
+
maxRatio: 200,
|
|
65
|
+
},
|
|
66
|
+
})
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
These defaults apply to every request but can still be overridden per-request via `useRequest()` (see [request.md](request.md)).
|
|
70
|
+
|
|
71
|
+
## Using with an Existing Node.js Server
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
import http from 'http'
|
|
75
|
+
import { createHttpApp } from '@wooksjs/event-http'
|
|
76
|
+
|
|
77
|
+
const app = createHttpApp()
|
|
78
|
+
app.get('/hello', () => 'Hello World!')
|
|
79
|
+
|
|
80
|
+
const server = http.createServer(app.getServerCb())
|
|
81
|
+
server.listen(3000)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## How HTTP Context Works
|
|
85
|
+
|
|
86
|
+
When a request arrives, the adapter creates an HTTP-specific event context:
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
Request arrives (req, res)
|
|
90
|
+
→ createHttpContext({ req, res }, options)
|
|
91
|
+
→ AsyncLocalStorage.run(httpContextStore, handler)
|
|
92
|
+
→ router matches path → handler runs
|
|
93
|
+
→ handler calls useRequest(), useCookies(), etc.
|
|
94
|
+
→ each composable calls useHttpContext()
|
|
95
|
+
→ reads/writes the HTTP context store via init(), get(), set()
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### The HTTP Context Store
|
|
99
|
+
|
|
100
|
+
The HTTP adapter extends the base event context with these sections:
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
interface THttpContextStore {
|
|
104
|
+
searchParams?: TSearchParamsCache // cached query params
|
|
105
|
+
cookies?: Record<string, string | null> // cached parsed cookies
|
|
106
|
+
setCookies?: Record<string, TSetCookieData> // outgoing response cookies
|
|
107
|
+
accept?: Record<string, boolean> // cached Accept header checks
|
|
108
|
+
authorization?: TAuthCache // cached auth header parsing
|
|
109
|
+
setHeader?: Record<string, string | string[]> // outgoing response headers
|
|
110
|
+
request?: TRequestCache // cached request data (body, IP, etc.)
|
|
111
|
+
response?: { responded: boolean } // response sent flag
|
|
112
|
+
status?: { code: EHttpStatusCode } // response status code
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Every section is lazily initialized — it only exists when a composable first writes to it. This means zero overhead for unused features.
|
|
117
|
+
|
|
118
|
+
### Extending the HTTP Store for Custom Composables
|
|
119
|
+
|
|
120
|
+
When creating custom composables for HTTP, extend the store type via the generic parameter on `useHttpContext`:
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
import { useHttpContext } from '@wooksjs/event-http'
|
|
124
|
+
|
|
125
|
+
interface TMyStore {
|
|
126
|
+
myFeature?: {
|
|
127
|
+
parsedToken?: { userId: string; role: string } | null
|
|
128
|
+
isAdmin?: boolean
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function useMyFeature() {
|
|
133
|
+
const { store } = useHttpContext<TMyStore>()
|
|
134
|
+
const { init } = store('myFeature')
|
|
135
|
+
|
|
136
|
+
const parsedToken = () =>
|
|
137
|
+
init('parsedToken', () => {
|
|
138
|
+
const { authRawCredentials, isBearer } = useAuthorization()
|
|
139
|
+
if (!isBearer()) return null
|
|
140
|
+
return decodeToken(authRawCredentials()!)
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
const isAdmin = () =>
|
|
144
|
+
init('isAdmin', () => parsedToken()?.role === 'admin')
|
|
145
|
+
|
|
146
|
+
return { parsedToken, isAdmin }
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
For the full context store API and more composable patterns, see [event-core.md](event-core.md).
|
|
151
|
+
|
|
152
|
+
## Server Lifecycle
|
|
153
|
+
|
|
154
|
+
### `listen(...)`
|
|
155
|
+
|
|
156
|
+
Starts a built-in HTTP server:
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
await app.listen(3000)
|
|
160
|
+
await app.listen(3000, '0.0.0.0')
|
|
161
|
+
await app.listen({ port: 3000, host: '0.0.0.0' })
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### `close(server?)`
|
|
165
|
+
|
|
166
|
+
Stops the server:
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
await app.close()
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### `getServer()`
|
|
173
|
+
|
|
174
|
+
Returns the underlying `http.Server` instance (only available after `listen()`):
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
const server = app.getServer()
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### `attachServer(server)`
|
|
181
|
+
|
|
182
|
+
Attaches an external server so `close()` can stop it:
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
const server = http.createServer(app.getServerCb())
|
|
186
|
+
app.attachServer(server)
|
|
187
|
+
server.listen(3000)
|
|
188
|
+
// later: await app.close()
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### `getServerCb()`
|
|
192
|
+
|
|
193
|
+
Returns the raw `(req, res) => void` callback for use with any Node.js HTTP server:
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
const cb = app.getServerCb()
|
|
197
|
+
http.createServer(cb).listen(3000)
|
|
198
|
+
// or with https:
|
|
199
|
+
https.createServer(sslOpts, cb).listen(443)
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Sharing Router Between Adapters
|
|
203
|
+
|
|
204
|
+
Multiple adapters can share the same Wooks router:
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
import { Wooks } from 'wooks'
|
|
208
|
+
import { createHttpApp } from '@wooksjs/event-http'
|
|
209
|
+
|
|
210
|
+
const wooks = new Wooks()
|
|
211
|
+
const app1 = createHttpApp({}, wooks)
|
|
212
|
+
const app2 = createHttpApp({}, wooks) // shares the same routes
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Or share via another adapter instance:
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
const app1 = createHttpApp()
|
|
219
|
+
const app2 = createHttpApp({}, app1) // shares app1's router
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## Logging
|
|
223
|
+
|
|
224
|
+
Get a scoped logger from the app:
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
const app = createHttpApp()
|
|
228
|
+
const logger = app.getLogger('[my-app]')
|
|
229
|
+
logger.log('App started')
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Inside a handler, use the event-scoped logger:
|
|
233
|
+
|
|
234
|
+
```ts
|
|
235
|
+
import { useEventLogger } from '@wooksjs/event-core'
|
|
236
|
+
|
|
237
|
+
app.get('/process', () => {
|
|
238
|
+
const logger = useEventLogger('my-handler')
|
|
239
|
+
logger.log('Processing request') // tagged with event ID
|
|
240
|
+
return 'ok'
|
|
241
|
+
})
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
## Testing
|
|
245
|
+
|
|
246
|
+
`@wooksjs/event-http` exports a test utility for running composables outside a real server:
|
|
247
|
+
|
|
248
|
+
```ts
|
|
249
|
+
import { prepareTestHttpContext } from '@wooksjs/event-http'
|
|
250
|
+
|
|
251
|
+
const runInContext = prepareTestHttpContext({
|
|
252
|
+
url: '/users/42',
|
|
253
|
+
method: 'GET',
|
|
254
|
+
headers: { authorization: 'Bearer test-token' },
|
|
255
|
+
params: { id: '42' },
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
runInContext(() => {
|
|
259
|
+
const { get } = useRouteParams()
|
|
260
|
+
console.log(get('id')) // '42'
|
|
261
|
+
|
|
262
|
+
const { isBearer } = useAuthorization()
|
|
263
|
+
console.log(isBearer()) // true
|
|
264
|
+
})
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### `prepareTestHttpContext(options)`
|
|
268
|
+
|
|
269
|
+
```ts
|
|
270
|
+
interface TTestHttpContext {
|
|
271
|
+
url: string
|
|
272
|
+
method?: string // default: 'GET'
|
|
273
|
+
headers?: Record<string, string>
|
|
274
|
+
params?: Record<string, string | string[]>
|
|
275
|
+
requestLimits?: TRequestLimits // app-level body limits for testing
|
|
276
|
+
cachedContext?: {
|
|
277
|
+
cookies?: Record<string, string | null>
|
|
278
|
+
authorization?: TAuthCache
|
|
279
|
+
body?: unknown // pre-parsed body
|
|
280
|
+
rawBody?: string | Buffer | Promise<Buffer>
|
|
281
|
+
raw?: Partial<THttpContextStore> // raw store sections
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
## Best Practices
|
|
287
|
+
|
|
288
|
+
- **Use `createHttpApp()` factory** — Don't instantiate `WooksHttp` directly unless you need to extend the class.
|
|
289
|
+
- **Use `getServerCb()` for custom servers** — This gives you full control over HTTPS, HTTP/2, or any custom server setup.
|
|
290
|
+
- **One composable per concern** — Split auth, validation, user-fetching into separate composables. Compose them in handlers.
|
|
291
|
+
- **Use `prepareTestHttpContext()` for unit testing composables** — Test composable logic without starting a server.
|
|
292
|
+
|
|
293
|
+
## Gotchas
|
|
294
|
+
|
|
295
|
+
- Composables must be called within a request handler (inside the async context). Calling them at module load time throws.
|
|
296
|
+
- `listen()` returns a promise — always `await` it or attach error handling.
|
|
297
|
+
- The framework auto-detects content type from your return value (string -> text/plain, object -> application/json). Override with `useSetHeaders().setContentType()`.
|
|
@@ -0,0 +1,253 @@
|
|
|
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.
|