@wooksjs/event-http 0.6.2 → 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.
@@ -0,0 +1,220 @@
1
+ # Request Utilities — @wooksjs/event-http
2
+
3
+ > Covers composables for reading incoming request data: headers, cookies, query params, authorization, IP address, and Accept header.
4
+
5
+ ## `useRequest()`
6
+
7
+ Primary composable for accessing the raw incoming HTTP request.
8
+
9
+ ```ts
10
+ import { useRequest } from '@wooksjs/event-http'
11
+
12
+ app.get('/info', () => {
13
+ const { method, url, headers, rawBody, reqId, getIp } = useRequest()
14
+
15
+ return {
16
+ method, // 'GET'
17
+ url, // '/info?page=1'
18
+ host: headers.host,
19
+ ip: getIp(),
20
+ requestId: reqId(),
21
+ }
22
+ })
23
+ ```
24
+
25
+ ### Properties & methods
26
+
27
+ | Name | Type | Description |
28
+ |------|------|-------------|
29
+ | `rawRequest` | `IncomingMessage` | The raw Node.js request object |
30
+ | `method` | `string` | HTTP method (GET, POST, etc.) |
31
+ | `url` | `string` | Raw request URL including query string |
32
+ | `headers` | `IncomingHttpHeaders` | Request headers object |
33
+ | `rawBody()` | `() => Promise<Buffer>` | Lazily reads and decompresses the request body |
34
+ | `reqId()` | `() => string` | Lazily generates a UUID for this request |
35
+ | `getIp(opts?)` | `(opts?) => string` | Returns client IP (supports `trustProxy`) |
36
+ | `getIpList()` | `() => { remoteIp, forwarded }` | Returns all known IPs |
37
+ | `isCompressed()` | `() => boolean` | Whether the body is compressed |
38
+
39
+ ### Body size limits
40
+
41
+ | Limit | Default | Description |
42
+ |-------|---------|-------------|
43
+ | `maxCompressed` | 1 MB (1 048 576) | Max compressed body size in bytes |
44
+ | `maxInflated` | 10 MB (10 485 760) | Max decompressed body size in bytes |
45
+ | `maxRatio` | 100 | Max compression ratio (zip-bomb protection) |
46
+ | `readTimeoutMs` | 10 000 | Body read timeout in milliseconds |
47
+
48
+ Limits can be set **app-wide** via `createHttpApp({ requestLimits: { ... } })` (see [core.md](core.md)) or **per-request** via the setters below (which override app defaults):
49
+
50
+ ```ts
51
+ const {
52
+ getMaxCompressed, setMaxCompressed, // default: 1 MB
53
+ getMaxInflated, setMaxInflated, // default: 10 MB
54
+ getMaxRatio, setMaxRatio, // default: 100× (zip-bomb protection)
55
+ getReadTimeoutMs, setReadTimeoutMs, // default: 10s
56
+ } = useRequest()
57
+
58
+ // Override per-route for file uploads
59
+ setMaxCompressed(50 * 1024 * 1024) // 50 MB
60
+ setMaxInflated(100 * 1024 * 1024) // 100 MB
61
+ setMaxRatio(200) // allow 200× compression ratio
62
+ ```
63
+
64
+ ### IP address with proxy support
65
+
66
+ ```ts
67
+ const { getIp } = useRequest()
68
+
69
+ // Direct connection IP
70
+ const ip = getIp()
71
+
72
+ // Trust X-Forwarded-For header (behind reverse proxy)
73
+ const clientIp = getIp({ trustProxy: true })
74
+ ```
75
+
76
+ ## `useHeaders()`
77
+
78
+ Returns incoming request headers directly:
79
+
80
+ ```ts
81
+ import { useHeaders } from '@wooksjs/event-http'
82
+
83
+ app.get('/check', () => {
84
+ const { host, authorization, 'content-type': contentType } = useHeaders()
85
+ return { host, hasAuth: !!authorization }
86
+ })
87
+ ```
88
+
89
+ Returns a standard `IncomingHttpHeaders` object (same as `req.headers`).
90
+
91
+ ## `useSearchParams()`
92
+
93
+ Access URL query parameters (lazy-parsed, cached):
94
+
95
+ ```ts
96
+ import { useSearchParams } from '@wooksjs/event-http'
97
+
98
+ app.get('/search', () => {
99
+ const { urlSearchParams, jsonSearchParams, rawSearchParams } = useSearchParams()
100
+
101
+ // URLSearchParams-like API
102
+ const page = urlSearchParams().get('page') // '1'
103
+ const tags = urlSearchParams().getAll('tag') // ['a', 'b']
104
+
105
+ // As a plain object (handles repeated keys as arrays)
106
+ const allParams = jsonSearchParams() // { page: '1', tag: ['a', 'b'] }
107
+
108
+ // Raw query string
109
+ const raw = rawSearchParams() // '?page=1&tag=a&tag=b'
110
+
111
+ return { page, tags, allParams }
112
+ })
113
+ ```
114
+
115
+ ## `useCookies()`
116
+
117
+ Parse incoming request cookies (lazy per-cookie parsing):
118
+
119
+ ```ts
120
+ import { useCookies } from '@wooksjs/event-http'
121
+
122
+ app.get('/dashboard', () => {
123
+ const { getCookie, rawCookies } = useCookies()
124
+
125
+ const session = getCookie('session_id') // 'abc123' or null
126
+ const theme = getCookie('theme') // 'dark' or null
127
+
128
+ return { session, theme }
129
+ })
130
+ ```
131
+
132
+ Each cookie is parsed individually on first access and cached. If you never call `getCookie('theme')`, the `theme` cookie is never parsed.
133
+
134
+ ## `useAuthorization()`
135
+
136
+ Parse the `Authorization` header (lazy, cached):
137
+
138
+ ```ts
139
+ import { useAuthorization } from '@wooksjs/event-http'
140
+
141
+ app.get('/protected', () => {
142
+ const { isBearer, isBasic, authType, authRawCredentials, basicCredentials } = useAuthorization()
143
+
144
+ if (isBearer()) {
145
+ const token = authRawCredentials() // 'eyJhbGciOi...'
146
+ // validate JWT
147
+ }
148
+
149
+ if (isBasic()) {
150
+ const { username, password } = basicCredentials()!
151
+ // validate credentials
152
+ }
153
+
154
+ return { authType: authType() } // 'Bearer', 'Basic', etc.
155
+ })
156
+ ```
157
+
158
+ ### Methods
159
+
160
+ | Name | Returns | Description |
161
+ |------|---------|-------------|
162
+ | `authorization` | `string \| undefined` | Raw header value |
163
+ | `authType()` | `string \| null` | Auth scheme (Bearer, Basic, etc.) |
164
+ | `authRawCredentials()` | `string \| null` | Everything after the scheme |
165
+ | `isBearer()` | `boolean` | True if Bearer auth |
166
+ | `isBasic()` | `boolean` | True if Basic auth |
167
+ | `basicCredentials()` | `{ username, password } \| null` | Decoded Basic credentials |
168
+
169
+ ## `useAccept()`
170
+
171
+ Check the `Accept` header for content negotiation:
172
+
173
+ ```ts
174
+ import { useAccept } from '@wooksjs/event-http'
175
+
176
+ app.get('/data', () => {
177
+ const { acceptsJson, acceptsHtml, acceptsXml, acceptsText, accepts } = useAccept()
178
+
179
+ if (acceptsJson()) {
180
+ return { data: 'json response' }
181
+ }
182
+ if (acceptsHtml()) {
183
+ return '<html><body>HTML response</body></html>'
184
+ }
185
+ if (accepts('image/png')) {
186
+ // custom MIME check
187
+ }
188
+
189
+ return 'plain text fallback'
190
+ })
191
+ ```
192
+
193
+ ## `useEventId()`
194
+
195
+ Generate a unique UUID for the current request (lazy, cached):
196
+
197
+ ```ts
198
+ import { useEventId } from '@wooksjs/event-http'
199
+
200
+ app.get('/track', () => {
201
+ const { getId } = useEventId()
202
+ return { requestId: getId() } // '550e8400-e29b-41d4-a716-446655440000'
203
+ })
204
+ ```
205
+
206
+ The UUID is generated on first call to `getId()` and cached for the request lifetime.
207
+
208
+ ## Best Practices
209
+
210
+ - **Use `useHeaders()` for raw header access**, `useAuthorization()` / `useCookies()` / `useAccept()` for parsed access. Don't manually parse headers when a composable exists.
211
+ - **Call `rawBody()` only once per request** — it reads the stream and returns a Buffer. The result is cached, so subsequent calls return the same promise.
212
+ - **Set limits before reading the body** — Call `setMaxCompressed()` etc. before `rawBody()` if you need non-default limits.
213
+ - **Use `getIp({ trustProxy: true })` only behind a trusted reverse proxy** — otherwise clients can spoof the `X-Forwarded-For` header.
214
+
215
+ ## Gotchas
216
+
217
+ - `useHeaders()` returns the raw Node.js `IncomingHttpHeaders` where all header names are lowercase.
218
+ - `getCookie()` returns `null` (not `undefined`) when a cookie doesn't exist.
219
+ - `rawBody()` returns a `Promise<Buffer>`. If the body is compressed (gzip/deflate/br), it is automatically decompressed.
220
+ - Body reading has a 10-second timeout by default. If the client sends data slowly, you may need to increase it with `setReadTimeoutMs()`.
@@ -0,0 +1,336 @@
1
+ # Response & Status — @wooksjs/event-http
2
+
3
+ > Covers setting status codes, response headers, outgoing cookies, cache control, and content type.
4
+
5
+ ## Concepts
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.
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.
10
+
11
+ ## `useResponse()`
12
+
13
+ Core response composable for status codes and raw response access:
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:
36
+
37
+ ```ts
38
+ const { status } = useResponse()
39
+
40
+ // Set status
41
+ status(404)
42
+
43
+ // Read status (call without args)
44
+ const currentStatus = status()
45
+
46
+ // Or use .value (hooked property)
47
+ status.value = 200
48
+ console.log(status.value) // 200
49
+ ```
50
+
51
+ ### Raw response access
52
+
53
+ ```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')
64
+ ```
65
+
66
+ ## `useStatus()`
67
+
68
+ Standalone status hook — returns a hookable accessor for the status code:
69
+
70
+ ```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
+ })
78
+ ```
79
+
80
+ This is useful when a utility function needs to set the status without pulling in the full `useResponse()`.
81
+
82
+ ### Type: `TStatusHook`
83
+
84
+ ```ts
85
+ import type { TStatusHook } from '@wooksjs/event-http'
86
+
87
+ function myMiddleware(status: TStatusHook) {
88
+ status.value = 403
89
+ }
90
+ ```
91
+
92
+ ## `useSetHeaders()`
93
+
94
+ Set outgoing response headers:
95
+
96
+ ```ts
97
+ import { useSetHeaders } from '@wooksjs/event-http'
98
+
99
+ app.get('/data', () => {
100
+ const { setHeader, setContentType, enableCors } = useSetHeaders()
101
+
102
+ setHeader('x-request-id', '12345')
103
+ setContentType('application/xml')
104
+ enableCors('https://example.com')
105
+
106
+ return '<data>hello</data>'
107
+ })
108
+ ```
109
+
110
+ ### Methods
111
+
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 `*`) |
120
+
121
+ ## `useSetHeader(name)`
122
+
123
+ Returns a hookable accessor for a single response header:
124
+
125
+ ```ts
126
+ import { useSetHeader } from '@wooksjs/event-http'
127
+
128
+ const xRequestId = useSetHeader('x-request-id')
129
+ xRequestId.value = '12345'
130
+ console.log(xRequestId.value) // '12345'
131
+ console.log(xRequestId.isDefined) // true
132
+ ```
133
+
134
+ ### Type: `THeaderHook`
135
+
136
+ ```ts
137
+ import type { THeaderHook } from '@wooksjs/event-http'
138
+
139
+ function setCorrelationId(header: THeaderHook) {
140
+ if (!header.isDefined) {
141
+ header.value = generateId()
142
+ }
143
+ }
144
+ ```
145
+
146
+ ## `useSetCookies()`
147
+
148
+ Set outgoing response cookies:
149
+
150
+ ```ts
151
+ import { useSetCookies } from '@wooksjs/event-http'
152
+
153
+ app.post('/login', () => {
154
+ const { setCookie, removeCookie, clearCookies } = useSetCookies()
155
+
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
+ })
163
+
164
+ return { success: true }
165
+ })
166
+ ```
167
+
168
+ ### Methods
169
+
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 |
177
+
178
+ ### Cookie Attributes
179
+
180
+ ```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'
189
+ }
190
+ ```
191
+
192
+ Time strings (`TTimeMultiString`) support formats like `'1h'`, `'30m'`, `'7d'`, `'1y'`.
193
+
194
+ ## `useSetCookie(name)`
195
+
196
+ Hookable accessor for a single outgoing cookie:
197
+
198
+ ```ts
199
+ import { useSetCookie } from '@wooksjs/event-http'
200
+
201
+ const sessionCookie = useSetCookie('session_id')
202
+
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 }
212
+ ```
213
+
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
+ ```
223
+
224
+ ## `useSetCacheControl()`
225
+
226
+ Set cache-related response headers:
227
+
228
+ ```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(...)
246
+ })
247
+ ```
248
+
249
+ ### Cache-Control Directives
250
+
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) |
280
+
281
+ Override by calling `setContentType()` before returning:
282
+
283
+ ```ts
284
+ app.get('/html', () => {
285
+ const { setContentType } = useSetHeaders()
286
+ setContentType('text/html')
287
+ return '<h1>Hello</h1>'
288
+ })
289
+ ```
290
+
291
+ ## Common Patterns
292
+
293
+ ### Pattern: JSON API Response
294
+
295
+ ```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
301
+ })
302
+ ```
303
+
304
+ ### Pattern: File Download
305
+
306
+ ```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')}`)
312
+ })
313
+ ```
314
+
315
+ ### Pattern: Redirect
316
+
317
+ ```ts
318
+ import { BaseHttpResponse } from '@wooksjs/event-http'
319
+
320
+ app.get('/old-page', () => {
321
+ return new BaseHttpResponse().setStatus(302).setHeader('location', '/new-page')
322
+ })
323
+ ```
324
+
325
+ ## Best Practices
326
+
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`.
331
+
332
+ ## Gotchas
333
+
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.