@wooksjs/event-http 0.6.6 → 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.
@@ -1,336 +1,267 @@
1
- # Response & Status — @wooksjs/event-http
1
+ # Response API — @wooksjs/event-http
2
2
 
3
- > Covers setting status codes, response headers, outgoing cookies, cache control, and content type.
3
+ > Status, headers, cookies, cache control, error handling, and response sending.
4
4
 
5
5
  ## Concepts
6
6
 
7
- In Wooks, the response is built through composables rather than mutating the `res` object directly. You call `useResponse()`, `useSetHeaders()`, `useSetCookies()`, etc. to configure the response. All settings are collected in the context store and applied when the framework sends the response.
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 framework automatically handles content type detection, serialization, and status code defaults. You only need to explicitly set these when you want non-default behavior.
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
- ## `useResponse()`
11
+ ## API Reference
12
12
 
13
- Core response composable for status codes and raw response access:
13
+ ### `useResponse(ctx?): HttpResponse`
14
14
 
15
- ```ts
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
- const { status } = useResponse()
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
- // Or use .value (hooked property)
47
- status.value = 200
48
- console.log(status.value) // 200
20
+ const response = useResponse()
21
+ response.setStatus(200).setHeader('x-custom', 'value')
49
22
  ```
50
23
 
51
- ### Raw response access
24
+ ### Status
52
25
 
53
26
  ```ts
54
- const { rawResponse } = useResponse()
55
-
56
- // Passthrough mode: lets you write directly but still uses framework headers
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
- ## `useStatus()`
32
+ If not set explicitly, status is inferred automatically (see core.md Auto-status).
67
33
 
68
- Standalone status hook — returns a hookable accessor for the status code:
34
+ ### Headers
69
35
 
70
36
  ```ts
71
- import { useStatus } from '@wooksjs/event-http'
72
-
73
- app.get('/check', () => {
74
- const statusHook = useStatus()
75
- statusHook.value = 202
76
- return 'Accepted'
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
- This is useful when a utility function needs to set the status without pulling in the full `useResponse()`.
48
+ ### Cookies (outgoing)
81
49
 
82
- ### Type: `TStatusHook`
50
+ Set-Cookie headers are managed via `HttpResponse`:
83
51
 
84
52
  ```ts
85
- import type { TStatusHook } from '@wooksjs/event-http'
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
- function myMiddleware(status: TStatusHook) {
88
- status.value = 403
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
- ## `useSetHeaders()`
93
-
94
- Set outgoing response headers:
95
-
96
- ```ts
97
- import { useSetHeaders } from '@wooksjs/event-http'
69
+ **`TCookieAttributes`:**
98
70
 
99
- app.get('/data', () => {
100
- const { setHeader, setContentType, enableCors } = useSetHeaders()
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
- setHeader('x-request-id', '12345')
103
- setContentType('application/xml')
104
- enableCors('https://example.com')
81
+ ### Cache control
105
82
 
106
- return '<data>hello</data>'
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
- ### Methods
99
+ ### Body and content type inference
111
100
 
112
- | Name | Signature | Description |
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
- ## `useSetHeader(name)`
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
- Returns a hookable accessor for a single response header:
113
+ ### Raw response access
114
+
115
+ For escape hatches (SSE, WebSocket upgrades, etc.):
124
116
 
125
117
  ```ts
126
- import { useSetHeader } from '@wooksjs/event-http'
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
- const xRequestId = useSetHeader('x-request-id')
129
- xRequestId.value = '12345'
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
- ### Type: `THeaderHook`
127
+ ### `responded` property
135
128
 
136
129
  ```ts
137
- import type { THeaderHook } from '@wooksjs/event-http'
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
- ## `useSetCookies()`
135
+ ## HttpError
147
136
 
148
- Set outgoing response cookies:
137
+ For error responses, throw `HttpError`:
149
138
 
150
139
  ```ts
151
- import { useSetCookies } from '@wooksjs/event-http'
140
+ import { HttpError } from '@wooksjs/event-http'
152
141
 
153
- app.post('/login', () => {
154
- const { setCookie, removeCookie, clearCookies } = useSetCookies()
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
- setCookie('session_id', 'abc123', {
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
- return { success: true }
165
- })
166
- ```
149
+ **Error rendering (`WooksHttpResponse`):**
167
150
 
168
- ### Methods
151
+ The default response class renders errors based on the `Accept` header:
169
152
 
170
- | Name | Signature | Description |
171
- |------|-----------|-------------|
172
- | `setCookie` | `(name, value, attrs?) => void` | Set a response cookie |
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
- ### Cookie Attributes
157
+ Override by providing a custom `responseClass` to `createHttpApp()`:
179
158
 
180
159
  ```ts
181
- interface TCookieAttributes {
182
- expires: Date | string | number // expiration date
183
- maxAge: number | TTimeMultiString // max age (seconds or time string)
184
- domain: string // cookie domain
185
- path: string // cookie path
186
- secure: boolean // HTTPS only
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
- Time strings (`TTimeMultiString`) support formats like `'1h'`, `'30m'`, `'7d'`, `'1y'`.
168
+ const app = createHttpApp({ responseClass: MyResponse })
169
+ ```
193
170
 
194
- ## `useSetCookie(name)`
171
+ ### Default headers and security headers
195
172
 
196
- Hookable accessor for a single outgoing cookie:
173
+ Pre-populate response headers for every request via `defaultHeaders`:
197
174
 
198
175
  ```ts
199
- import { useSetCookie } from '@wooksjs/event-http'
200
-
201
- const sessionCookie = useSetCookie('session_id')
176
+ import { createHttpApp, securityHeaders } from '@wooksjs/event-http'
202
177
 
203
- // Set value
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
- ### Type: `TCookieHook`
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
- ## `useSetCacheControl()`
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
- Set cache-related response headers:
192
+ Options: `string` (override) or `false` (disable). `strictTransportSecurity` is opt-in only.
227
193
 
228
194
  ```ts
229
- import { useSetCacheControl } from '@wooksjs/event-http'
230
-
231
- app.get('/assets/:file', () => {
232
- const { setCacheControl, setExpires, setAge, setPragmaNoCache } = useSetCacheControl()
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
- ### Cache-Control Directives
202
+ Per-endpoint: `response.setHeaders(securityHeaders({ ... }))`.
250
203
 
251
- ```ts
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
- Override by calling `setContentType()` before returning:
206
+ ### Pattern: Full response control
282
207
 
283
208
  ```ts
284
- app.get('/html', () => {
285
- const { setContentType } = useSetHeaders()
286
- setContentType('text/html')
287
- return '<h1>Hello</h1>'
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
- ## Common Patterns
292
-
293
- ### Pattern: JSON API Response
220
+ ### Pattern: Redirect
294
221
 
295
222
  ```ts
296
- app.get('/api/users/:id', async () => {
297
- const { get } = useRouteParams<{ id: string }>()
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: File Download
228
+ ### Pattern: Streaming response
305
229
 
306
230
  ```ts
307
- app.get('/download/:file', () => {
308
- const { setHeader } = useSetHeaders()
309
- const { get } = useRouteParams<{ file: string }>()
310
- setHeader('content-disposition', `attachment; filename="${get('file')}"`)
311
- return createReadStream(`/uploads/${get('file')}`)
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: Redirect
239
+ ### Pattern: Server-Sent Events
316
240
 
317
241
  ```ts
318
- import { BaseHttpResponse } from '@wooksjs/event-http'
319
-
320
- app.get('/old-page', () => {
321
- return new BaseHttpResponse().setStatus(302).setHeader('location', '/new-page')
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
- - **Let the framework auto-detect content type** Only call `setContentType()` when you need a non-default type.
328
- - **Use `useSetCookies()` for multiple cookies, `useSetCookie(name)` for hookable access to a single cookie**.
329
- - **Set status before returning** — If you need a custom status, call `status(code)` before the handler returns.
330
- - **Use time strings for maxAge** `'7d'` is clearer than `604800`.
256
+ - Return values for simple responsesonly 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
- - If you call `rawResponse()` without `{ passthrough: true }`, the framework marks the response as sent and won't write anything else.
335
- - Headers set via `useSetHeaders()` are merged with any `BaseHttpResponse` headers. The `BaseHttpResponse` headers take precedence on collision.
336
- - Cookies set via `useSetCookies()` are merged with `BaseHttpResponse.setCookie()` cookies.
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