@wooksjs/event-http 0.6.5 → 0.7.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 +11 -45
- package/dist/index.cjs +1167 -976
- package/dist/index.d.ts +361 -304
- package/dist/index.mjs +1106 -911
- 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 +130 -146
- 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,336 +1,267 @@
|
|
|
1
|
-
# Response
|
|
1
|
+
# Response API — @wooksjs/event-http
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> Status, headers, cookies, cache control, error handling, and response sending.
|
|
4
4
|
|
|
5
5
|
## Concepts
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
`useResponse()` returns an `HttpResponse` instance for the current request. All response operations — status, headers, cookies, cache control — are methods on this single object. Methods are chainable.
|
|
8
8
|
|
|
9
|
-
The
|
|
9
|
+
The response is also controlled implicitly by return values: returning an object sends JSON, returning a string sends text, returning nothing sends 204.
|
|
10
10
|
|
|
11
|
-
##
|
|
11
|
+
## API Reference
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
### `useResponse(ctx?): HttpResponse`
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
import { useResponse } from '@wooksjs/event-http'
|
|
17
|
-
|
|
18
|
-
app.post('/users', () => {
|
|
19
|
-
const { status } = useResponse()
|
|
20
|
-
status(201) // Set status to 201 Created
|
|
21
|
-
return { id: 1, name: 'Alice' }
|
|
22
|
-
})
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
### Properties & methods
|
|
26
|
-
|
|
27
|
-
| Name | Type | Description |
|
|
28
|
-
|------|------|-------------|
|
|
29
|
-
| `status(code?)` | `(code?) => EHttpStatusCode` | Get/set the response status code |
|
|
30
|
-
| `rawResponse(opts?)` | `(opts?) => ServerResponse` | Access the raw Node.js response |
|
|
31
|
-
| `hasResponded()` | `() => boolean` | True if response already sent |
|
|
32
|
-
|
|
33
|
-
### `status()` as a hookable function
|
|
34
|
-
|
|
35
|
-
The `status` function doubles as a hookable accessor:
|
|
15
|
+
Returns the `HttpResponse` for the current request.
|
|
36
16
|
|
|
37
17
|
```ts
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
// Set status
|
|
41
|
-
status(404)
|
|
42
|
-
|
|
43
|
-
// Read status (call without args)
|
|
44
|
-
const currentStatus = status()
|
|
18
|
+
import { useResponse } from '@wooksjs/event-http'
|
|
45
19
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
console.log(status.value) // 200
|
|
20
|
+
const response = useResponse()
|
|
21
|
+
response.setStatus(200).setHeader('x-custom', 'value')
|
|
49
22
|
```
|
|
50
23
|
|
|
51
|
-
###
|
|
24
|
+
### Status
|
|
52
25
|
|
|
53
26
|
```ts
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const res = rawResponse({ passthrough: true })
|
|
58
|
-
res.write('chunk 1')
|
|
59
|
-
|
|
60
|
-
// Default mode: marks response as "handled", framework won't write again
|
|
61
|
-
const res2 = rawResponse()
|
|
62
|
-
res2.writeHead(200)
|
|
63
|
-
res2.end('done')
|
|
27
|
+
response.status = 201 // set via property
|
|
28
|
+
response.setStatus(201) // set via method (chainable)
|
|
29
|
+
const code = response.status // get current status
|
|
64
30
|
```
|
|
65
31
|
|
|
66
|
-
|
|
32
|
+
If not set explicitly, status is inferred automatically (see core.md Auto-status).
|
|
67
33
|
|
|
68
|
-
|
|
34
|
+
### Headers
|
|
69
35
|
|
|
70
36
|
```ts
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
37
|
+
response.setHeader('x-custom', 'value') // set a header (chainable)
|
|
38
|
+
response.setHeaders({ 'x-one': '1', 'x-two': '2' }) // batch-set headers
|
|
39
|
+
response.setHeader('x-multi', ['a', 'b']) // multi-value header
|
|
40
|
+
response.getHeader('x-custom') // get header value
|
|
41
|
+
response.removeHeader('x-custom') // remove a header (chainable)
|
|
42
|
+
response.headers() // all headers as Record
|
|
43
|
+
response.setContentType('application/xml') // shorthand for content-type
|
|
44
|
+
response.getContentType() // get content-type
|
|
45
|
+
response.enableCors('*') // set Access-Control-Allow-Origin (chainable)
|
|
78
46
|
```
|
|
79
47
|
|
|
80
|
-
|
|
48
|
+
### Cookies (outgoing)
|
|
81
49
|
|
|
82
|
-
|
|
50
|
+
Set-Cookie headers are managed via `HttpResponse`:
|
|
83
51
|
|
|
84
52
|
```ts
|
|
85
|
-
|
|
53
|
+
response.setCookie('session', 'abc123', {
|
|
54
|
+
httpOnly: true,
|
|
55
|
+
secure: true,
|
|
56
|
+
sameSite: 'Strict',
|
|
57
|
+
maxAge: 3600, // seconds (also accepts time strings)
|
|
58
|
+
path: '/',
|
|
59
|
+
domain: '.example.com',
|
|
60
|
+
expires: new Date('2025-12-31'),
|
|
61
|
+
})
|
|
86
62
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
63
|
+
response.getCookie('session') // { value, attrs } or undefined
|
|
64
|
+
response.removeCookie('session')
|
|
65
|
+
response.clearCookies()
|
|
66
|
+
response.setCookieRaw('name=value; Path=/; HttpOnly') // raw Set-Cookie string
|
|
90
67
|
```
|
|
91
68
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
Set outgoing response headers:
|
|
95
|
-
|
|
96
|
-
```ts
|
|
97
|
-
import { useSetHeaders } from '@wooksjs/event-http'
|
|
69
|
+
**`TCookieAttributes`:**
|
|
98
70
|
|
|
99
|
-
|
|
100
|
-
|
|
71
|
+
| Attribute | Type | Description |
|
|
72
|
+
| ---------- | ---------------------------------------- | ------------------ |
|
|
73
|
+
| `expires` | `Date \| string \| number` | Expiration date |
|
|
74
|
+
| `maxAge` | `number \| TTimeMultiString` | Max age in seconds |
|
|
75
|
+
| `domain` | `string` | Cookie domain |
|
|
76
|
+
| `path` | `string` | Cookie path |
|
|
77
|
+
| `secure` | `boolean` | Secure flag |
|
|
78
|
+
| `httpOnly` | `boolean` | HttpOnly flag |
|
|
79
|
+
| `sameSite` | `boolean \| 'Lax' \| 'None' \| 'Strict'` | SameSite policy |
|
|
101
80
|
|
|
102
|
-
|
|
103
|
-
setContentType('application/xml')
|
|
104
|
-
enableCors('https://example.com')
|
|
81
|
+
### Cache control
|
|
105
82
|
|
|
106
|
-
|
|
83
|
+
```ts
|
|
84
|
+
response.setCacheControl({
|
|
85
|
+
public: true,
|
|
86
|
+
maxAge: 3600,
|
|
87
|
+
sMaxage: 7200,
|
|
88
|
+
noStore: false,
|
|
89
|
+
noCache: false,
|
|
90
|
+
mustRevalidate: true,
|
|
107
91
|
})
|
|
92
|
+
|
|
93
|
+
response.setAge(300) // Age header (seconds)
|
|
94
|
+
response.setExpires(new Date('2025-12-31')) // Expires header
|
|
95
|
+
response.setExpires('2025-12-31') // also accepts strings
|
|
96
|
+
response.setPragmaNoCache() // Pragma: no-cache
|
|
108
97
|
```
|
|
109
98
|
|
|
110
|
-
###
|
|
99
|
+
### Body and content type inference
|
|
111
100
|
|
|
112
|
-
|
|
113
|
-
|------|-----------|-------------|
|
|
114
|
-
| `setHeader` | `(name, value) => void` | Set a response header |
|
|
115
|
-
| `getHeader` | `(name) => string \| undefined` | Read a previously set header |
|
|
116
|
-
| `removeHeader` | `(name) => void` | Remove a set header |
|
|
117
|
-
| `setContentType` | `(value) => void` | Shortcut for `setHeader('content-type', value)` |
|
|
118
|
-
| `headers` | `() => Record<string, string>` | Get all set headers as an object |
|
|
119
|
-
| `enableCors` | `(origin?) => void` | Set `Access-Control-Allow-Origin` (default `*`) |
|
|
101
|
+
Return values are automatically serialized:
|
|
120
102
|
|
|
121
|
-
|
|
103
|
+
| Return type | Content-Type | Status |
|
|
104
|
+
| -------------------------------- | --------------------- | -------------- |
|
|
105
|
+
| `string` | `text/plain` | Auto |
|
|
106
|
+
| `number` / `boolean` | `text/plain` | Auto |
|
|
107
|
+
| `object` / `array` | `application/json` | Auto |
|
|
108
|
+
| `Buffer` / `Uint8Array` | (none — set manually) | Auto |
|
|
109
|
+
| `Readable` stream | (none — set manually) | Auto |
|
|
110
|
+
| `Response` (fetch) | From fetch response | Auto |
|
|
111
|
+
| `undefined` / `null` / no return | — | 204 No Content |
|
|
122
112
|
|
|
123
|
-
|
|
113
|
+
### Raw response access
|
|
114
|
+
|
|
115
|
+
For escape hatches (SSE, WebSocket upgrades, etc.):
|
|
124
116
|
|
|
125
117
|
```ts
|
|
126
|
-
|
|
118
|
+
// Take full control — framework won't send anything
|
|
119
|
+
const res = response.getRawRes()
|
|
120
|
+
res.writeHead(200, { 'content-type': 'text/event-stream' })
|
|
121
|
+
res.write('data: hello\n\n')
|
|
127
122
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
console.log(xRequestId.value) // '12345'
|
|
131
|
-
console.log(xRequestId.isDefined) // true
|
|
123
|
+
// Passthrough — you write to res, but framework still finalizes
|
|
124
|
+
const res = response.getRawRes(true)
|
|
132
125
|
```
|
|
133
126
|
|
|
134
|
-
###
|
|
127
|
+
### `responded` property
|
|
135
128
|
|
|
136
129
|
```ts
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
function setCorrelationId(header: THeaderHook) {
|
|
140
|
-
if (!header.isDefined) {
|
|
141
|
-
header.value = generateId()
|
|
142
|
-
}
|
|
130
|
+
if (response.responded) {
|
|
131
|
+
// Response already sent — don't try to send again
|
|
143
132
|
}
|
|
144
133
|
```
|
|
145
134
|
|
|
146
|
-
##
|
|
135
|
+
## HttpError
|
|
147
136
|
|
|
148
|
-
|
|
137
|
+
For error responses, throw `HttpError`:
|
|
149
138
|
|
|
150
139
|
```ts
|
|
151
|
-
import {
|
|
140
|
+
import { HttpError } from '@wooksjs/event-http'
|
|
152
141
|
|
|
153
|
-
|
|
154
|
-
|
|
142
|
+
throw new HttpError(404) // 404 with default message
|
|
143
|
+
throw new HttpError(400, 'Invalid email') // 400 with custom message
|
|
144
|
+
throw new HttpError(422, { message: 'Validation failed', fields: ['email'] }) // structured body
|
|
145
|
+
```
|
|
155
146
|
|
|
156
|
-
|
|
157
|
-
httpOnly: true,
|
|
158
|
-
secure: true,
|
|
159
|
-
sameSite: 'Strict',
|
|
160
|
-
maxAge: '7d', // supports time strings like '1h', '30m', '7d'
|
|
161
|
-
path: '/',
|
|
162
|
-
})
|
|
147
|
+
`HttpError` skips stack trace capture for performance (these are expected control-flow errors).
|
|
163
148
|
|
|
164
|
-
|
|
165
|
-
})
|
|
166
|
-
```
|
|
149
|
+
**Error rendering (`WooksHttpResponse`):**
|
|
167
150
|
|
|
168
|
-
|
|
151
|
+
The default response class renders errors based on the `Accept` header:
|
|
169
152
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
| `getCookie` | `(name) => TSetCookieData \| undefined` | Read a previously set cookie |
|
|
174
|
-
| `removeCookie` | `(name) => void` | Remove a set cookie |
|
|
175
|
-
| `clearCookies` | `() => void` | Remove all set cookies |
|
|
176
|
-
| `cookies` | `() => string[]` | Render all cookies as Set-Cookie header strings |
|
|
153
|
+
- `application/json` → JSON `{ statusCode, message, error }`
|
|
154
|
+
- `text/html` → Styled HTML error page with SVG icons
|
|
155
|
+
- `text/plain` → Plain text error
|
|
177
156
|
|
|
178
|
-
|
|
157
|
+
Override by providing a custom `responseClass` to `createHttpApp()`:
|
|
179
158
|
|
|
180
159
|
```ts
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
httpOnly: boolean // no JS access
|
|
188
|
-
sameSite: boolean | 'Lax' | 'None' | 'Strict'
|
|
160
|
+
class MyResponse extends WooksHttpResponse {
|
|
161
|
+
protected renderError(data, ctx) {
|
|
162
|
+
this._status = data.statusCode
|
|
163
|
+
this._headers['content-type'] = 'application/json'
|
|
164
|
+
this._body = JSON.stringify({ error: data.message })
|
|
165
|
+
}
|
|
189
166
|
}
|
|
190
|
-
```
|
|
191
167
|
|
|
192
|
-
|
|
168
|
+
const app = createHttpApp({ responseClass: MyResponse })
|
|
169
|
+
```
|
|
193
170
|
|
|
194
|
-
|
|
171
|
+
### Default headers and security headers
|
|
195
172
|
|
|
196
|
-
|
|
173
|
+
Pre-populate response headers for every request via `defaultHeaders`:
|
|
197
174
|
|
|
198
175
|
```ts
|
|
199
|
-
import {
|
|
200
|
-
|
|
201
|
-
const sessionCookie = useSetCookie('session_id')
|
|
176
|
+
import { createHttpApp, securityHeaders } from '@wooksjs/event-http'
|
|
202
177
|
|
|
203
|
-
|
|
204
|
-
sessionCookie.value = 'abc123'
|
|
205
|
-
|
|
206
|
-
// Set attributes
|
|
207
|
-
sessionCookie.attrs = { httpOnly: true, secure: true }
|
|
208
|
-
|
|
209
|
-
// Read
|
|
210
|
-
console.log(sessionCookie.value) // 'abc123'
|
|
211
|
-
console.log(sessionCookie.attrs) // { httpOnly: true, secure: true }
|
|
178
|
+
const app = createHttpApp({ defaultHeaders: securityHeaders() })
|
|
212
179
|
```
|
|
213
180
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
```ts
|
|
217
|
-
import type { TCookieHook } from '@wooksjs/event-http'
|
|
218
|
-
|
|
219
|
-
function enforceSecureCookie(cookie: TCookieHook) {
|
|
220
|
-
cookie.attrs = { ...cookie.attrs, secure: true, httpOnly: true }
|
|
221
|
-
}
|
|
222
|
-
```
|
|
181
|
+
`securityHeaders(opts?)` returns recommended HTTP security headers:
|
|
223
182
|
|
|
224
|
-
|
|
183
|
+
| Header | Default |
|
|
184
|
+
| ------------------------------ | --------------------------------------------------------------------------------- |
|
|
185
|
+
| `content-security-policy` | `default-src 'self'; base-uri 'self'; form-action 'self'; frame-ancestors 'self'` |
|
|
186
|
+
| `cross-origin-opener-policy` | `same-origin` |
|
|
187
|
+
| `cross-origin-resource-policy` | `same-origin` |
|
|
188
|
+
| `referrer-policy` | `no-referrer` |
|
|
189
|
+
| `x-content-type-options` | `nosniff` |
|
|
190
|
+
| `x-frame-options` | `SAMEORIGIN` |
|
|
225
191
|
|
|
226
|
-
|
|
192
|
+
Options: `string` (override) or `false` (disable). `strictTransportSecurity` is opt-in only.
|
|
227
193
|
|
|
228
194
|
```ts
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
setCacheControl({
|
|
235
|
-
maxAge: 3600,
|
|
236
|
-
public: true,
|
|
237
|
-
noTransform: true,
|
|
238
|
-
})
|
|
239
|
-
|
|
240
|
-
// Or set individual cache headers
|
|
241
|
-
setAge(300) // Age: 300
|
|
242
|
-
setExpires(new Date('2025-12-31')) // Expires: Wed, 31 Dec 2025 ...
|
|
243
|
-
setPragmaNoCache() // Pragma: no-cache
|
|
244
|
-
|
|
245
|
-
return serveFile(...)
|
|
195
|
+
securityHeaders({
|
|
196
|
+
contentSecurityPolicy: false,
|
|
197
|
+
referrerPolicy: 'strict-origin-when-cross-origin',
|
|
198
|
+
strictTransportSecurity: 'max-age=31536000; includeSubDomains',
|
|
246
199
|
})
|
|
247
200
|
```
|
|
248
201
|
|
|
249
|
-
|
|
202
|
+
Per-endpoint: `response.setHeaders(securityHeaders({ ... }))`.
|
|
250
203
|
|
|
251
|
-
|
|
252
|
-
interface TCacheControl {
|
|
253
|
-
maxAge?: number | TTimeMultiString
|
|
254
|
-
sMaxage?: number | TTimeMultiString
|
|
255
|
-
noCache?: boolean
|
|
256
|
-
noStore?: boolean
|
|
257
|
-
noTransform?: boolean
|
|
258
|
-
mustRevalidate?: boolean
|
|
259
|
-
proxyRevalidate?: boolean
|
|
260
|
-
public?: boolean
|
|
261
|
-
private?: boolean
|
|
262
|
-
immutable?: boolean
|
|
263
|
-
staleWhileRevalidate?: number | TTimeMultiString
|
|
264
|
-
staleIfError?: number | TTimeMultiString
|
|
265
|
-
}
|
|
266
|
-
```
|
|
267
|
-
|
|
268
|
-
## Content Type Auto-Detection
|
|
269
|
-
|
|
270
|
-
The framework automatically sets the content type based on the handler return value:
|
|
271
|
-
|
|
272
|
-
| Return type | Content-Type |
|
|
273
|
-
|-------------|-------------|
|
|
274
|
-
| `string` | `text/plain` |
|
|
275
|
-
| `number` | `text/plain` |
|
|
276
|
-
| `boolean` | `text/plain` |
|
|
277
|
-
| `object` / `array` | `application/json` |
|
|
278
|
-
| `Readable` stream | (must set manually) |
|
|
279
|
-
| `undefined` | (no body, 204) |
|
|
204
|
+
## Common Patterns
|
|
280
205
|
|
|
281
|
-
|
|
206
|
+
### Pattern: Full response control
|
|
282
207
|
|
|
283
208
|
```ts
|
|
284
|
-
app.get('/
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
209
|
+
app.get('/data', () => {
|
|
210
|
+
useResponse()
|
|
211
|
+
.setStatus(200)
|
|
212
|
+
.setHeader('x-request-id', useRequest().reqId())
|
|
213
|
+
.setCookie('visited', 'true', { httpOnly: true })
|
|
214
|
+
.setCacheControl({ public: true, maxAge: 3600 })
|
|
215
|
+
|
|
216
|
+
return { data: 'hello' }
|
|
288
217
|
})
|
|
289
218
|
```
|
|
290
219
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
### Pattern: JSON API Response
|
|
220
|
+
### Pattern: Redirect
|
|
294
221
|
|
|
295
222
|
```ts
|
|
296
|
-
app.get('/
|
|
297
|
-
|
|
298
|
-
const user = await db.findUser(get('id'))
|
|
299
|
-
if (!user) throw new HttpError(404, 'User not found')
|
|
300
|
-
return user // auto-serialized as JSON with 200
|
|
223
|
+
app.get('/old-path', () => {
|
|
224
|
+
useResponse().setStatus(301).setHeader('location', '/new-path')
|
|
301
225
|
})
|
|
302
226
|
```
|
|
303
227
|
|
|
304
|
-
### Pattern:
|
|
228
|
+
### Pattern: Streaming response
|
|
305
229
|
|
|
306
230
|
```ts
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
return createReadStream(
|
|
231
|
+
import { createReadStream } from 'fs'
|
|
232
|
+
|
|
233
|
+
app.get('/file', () => {
|
|
234
|
+
useResponse().setContentType('application/octet-stream')
|
|
235
|
+
return createReadStream('/path/to/file')
|
|
312
236
|
})
|
|
313
237
|
```
|
|
314
238
|
|
|
315
|
-
### Pattern:
|
|
239
|
+
### Pattern: Server-Sent Events
|
|
316
240
|
|
|
317
241
|
```ts
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
242
|
+
app.get('/events', () => {
|
|
243
|
+
const res = useResponse().getRawRes()
|
|
244
|
+
res.writeHead(200, {
|
|
245
|
+
'content-type': 'text/event-stream',
|
|
246
|
+
'cache-control': 'no-cache',
|
|
247
|
+
connection: 'keep-alive',
|
|
248
|
+
})
|
|
249
|
+
// Write events...
|
|
250
|
+
return res // returning the raw response signals "already handled"
|
|
322
251
|
})
|
|
323
252
|
```
|
|
324
253
|
|
|
325
254
|
## Best Practices
|
|
326
255
|
|
|
327
|
-
-
|
|
328
|
-
-
|
|
329
|
-
-
|
|
330
|
-
-
|
|
256
|
+
- Return values for simple responses — only use `useResponse()` when you need headers, cookies, or explicit status
|
|
257
|
+
- Use `HttpError` for all error responses — don't manually set error status and body
|
|
258
|
+
- The chainable API means you can do `useResponse().setStatus(200).setHeader(...)` in one statement
|
|
259
|
+
- For custom error rendering, subclass `WooksHttpResponse` and override `renderError()`
|
|
331
260
|
|
|
332
261
|
## Gotchas
|
|
333
262
|
|
|
334
|
-
-
|
|
335
|
-
-
|
|
336
|
-
-
|
|
263
|
+
- Calling `send()` twice throws — check `response.responded` if unsure
|
|
264
|
+
- `getRawRes()` without `passthrough` marks the response as "responded" — the framework won't touch it
|
|
265
|
+
- `getRawRes(true)` (passthrough mode) lets you write headers/data while the framework still finalizes cookies and status
|
|
266
|
+
- Cookie `maxAge` is in seconds, not milliseconds
|
|
267
|
+
- `setContentType()` overwrites any previously set content-type
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# Testing — @wooksjs/event-http
|
|
2
|
+
|
|
3
|
+
> Writing tests for handlers and composables with `prepareTestHttpContext`.
|
|
4
|
+
|
|
5
|
+
## Concepts
|
|
6
|
+
|
|
7
|
+
`prepareTestHttpContext` creates a fully initialized HTTP event context for testing. It sets up an `EventContext` with `httpKind` seeds (fake `IncomingMessage`, `HttpResponse`, route params, and optional pre-seeded body), then returns a runner function that executes callbacks inside the context scope.
|
|
8
|
+
|
|
9
|
+
## API Reference
|
|
10
|
+
|
|
11
|
+
### `prepareTestHttpContext(options): (cb) => T`
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { prepareTestHttpContext } from '@wooksjs/event-http'
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
**Options (`TTestHttpContext`):**
|
|
18
|
+
|
|
19
|
+
| Option | Type | Required | Description |
|
|
20
|
+
| ---------------- | ------------------------------------ | -------- | ----------------------------------------------- |
|
|
21
|
+
| `url` | `string` | Yes | Request URL (e.g. `/api/users?page=1`) |
|
|
22
|
+
| `method` | `string` | No | HTTP method (default: `'GET'`) |
|
|
23
|
+
| `headers` | `Record<string, string>` | No | Request headers |
|
|
24
|
+
| `params` | `Record<string, string \| string[]>` | No | Pre-set route parameters |
|
|
25
|
+
| `requestLimits` | `TRequestLimits` | No | Custom request limits |
|
|
26
|
+
| `rawBody` | `string \| Buffer` | No | Pre-seed the raw body (skips stream reading) |
|
|
27
|
+
| `defaultHeaders` | `Record<string, string \| string[]>` | No | Default headers to pre-populate on the response |
|
|
28
|
+
|
|
29
|
+
**Returns:** `(cb: () => T) => T` — a runner function.
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
const run = prepareTestHttpContext({
|
|
33
|
+
url: '/users/42',
|
|
34
|
+
method: 'GET',
|
|
35
|
+
headers: { authorization: 'Bearer abc123' },
|
|
36
|
+
params: { id: '42' },
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
run(() => {
|
|
40
|
+
// All composables work here
|
|
41
|
+
const { method } = useRequest()
|
|
42
|
+
const { params } = useRouteParams()
|
|
43
|
+
const { authIs } = useAuthorization()
|
|
44
|
+
expect(method).toBe('GET')
|
|
45
|
+
expect(params.id).toBe('42')
|
|
46
|
+
expect(authIs('bearer')).toBe(true)
|
|
47
|
+
})
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Common Patterns
|
|
51
|
+
|
|
52
|
+
### Pattern: Testing a custom composable
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
import { describe, it, expect } from 'vitest'
|
|
56
|
+
import { prepareTestHttpContext, useHeaders } from '@wooksjs/event-http'
|
|
57
|
+
import { defineWook, cached } from '@wooksjs/event-core'
|
|
58
|
+
|
|
59
|
+
const useApiKey = defineWook((ctx) => {
|
|
60
|
+
const headers = useHeaders(ctx)
|
|
61
|
+
return {
|
|
62
|
+
apiKey: headers['x-api-key'] as string | undefined,
|
|
63
|
+
isValid: () => headers['x-api-key'] === 'secret',
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
describe('useApiKey', () => {
|
|
68
|
+
it('extracts API key from headers', () => {
|
|
69
|
+
const run = prepareTestHttpContext({
|
|
70
|
+
url: '/api/data',
|
|
71
|
+
headers: { 'x-api-key': 'secret' },
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
run(() => {
|
|
75
|
+
const { apiKey, isValid } = useApiKey()
|
|
76
|
+
expect(apiKey).toBe('secret')
|
|
77
|
+
expect(isValid()).toBe(true)
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Pattern: Testing body parsing
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
import { useBody } from '@wooksjs/http-body'
|
|
87
|
+
|
|
88
|
+
it('parses JSON body', async () => {
|
|
89
|
+
const run = prepareTestHttpContext({
|
|
90
|
+
url: '/api/users',
|
|
91
|
+
method: 'POST',
|
|
92
|
+
headers: { 'content-type': 'application/json' },
|
|
93
|
+
rawBody: JSON.stringify({ name: 'Alice' }),
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
await run(async () => {
|
|
97
|
+
const { parseBody } = useBody()
|
|
98
|
+
const body = await parseBody<{ name: string }>()
|
|
99
|
+
expect(body.name).toBe('Alice')
|
|
100
|
+
})
|
|
101
|
+
})
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Pattern: Testing response composable
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
it('sets response headers', () => {
|
|
108
|
+
const run = prepareTestHttpContext({ url: '/test' })
|
|
109
|
+
|
|
110
|
+
run(() => {
|
|
111
|
+
const response = useResponse()
|
|
112
|
+
response.setHeader('x-custom', 'value')
|
|
113
|
+
response.setCookie('session', 'abc', { httpOnly: true })
|
|
114
|
+
|
|
115
|
+
expect(response.getHeader('x-custom')).toBe('value')
|
|
116
|
+
expect(response.getCookie('session')?.value).toBe('abc')
|
|
117
|
+
})
|
|
118
|
+
})
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Pattern: Testing cookies
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
it('reads cookies from request', () => {
|
|
125
|
+
const run = prepareTestHttpContext({
|
|
126
|
+
url: '/dashboard',
|
|
127
|
+
headers: { cookie: 'session=abc123; theme=dark' },
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
run(() => {
|
|
131
|
+
const { getCookie } = useCookies()
|
|
132
|
+
expect(getCookie('session')).toBe('abc123')
|
|
133
|
+
expect(getCookie('theme')).toBe('dark')
|
|
134
|
+
expect(getCookie('missing')).toBeNull()
|
|
135
|
+
})
|
|
136
|
+
})
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Best Practices
|
|
140
|
+
|
|
141
|
+
- Always use `prepareTestHttpContext` — don't manually construct `EventContext` for HTTP tests
|
|
142
|
+
- Pre-seed `rawBody` for body parsing tests to avoid stream setup
|
|
143
|
+
- Pre-seed `params` to test route parameter logic without a router
|
|
144
|
+
- The runner function supports async callbacks — `await run(async () => { ... })`
|
|
145
|
+
|
|
146
|
+
## Gotchas
|
|
147
|
+
|
|
148
|
+
- `prepareTestHttpContext` uses a real `IncomingMessage` and `ServerResponse` (from `new Socket({})`) — they're functional but not connected to a network
|
|
149
|
+
- The `HttpResponse` in tests is the base `HttpResponse`, not `WooksHttpResponse` — error rendering won't do content negotiation
|
|
150
|
+
- `rawBody` pre-seeding stores a resolved Promise — `useRequest().rawBody()` returns immediately
|