@wooksjs/event-http 0.7.7 → 0.7.9
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/dist/index.cjs +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +7 -13
- package/scripts/setup-skills.js +0 -77
- package/skills/wooksjs-event-http/SKILL.md +0 -44
- package/skills/wooksjs-event-http/core.md +0 -152
- package/skills/wooksjs-event-http/request.md +0 -204
- package/skills/wooksjs-event-http/response.md +0 -267
- package/skills/wooksjs-event-http/testing.md +0 -150
package/dist/index.cjs
CHANGED
|
@@ -1449,7 +1449,7 @@ function error_tl_default(ctx) {
|
|
|
1449
1449
|
//#endregion
|
|
1450
1450
|
//#region packages/event-http/src/response/wooks-http-response.ts
|
|
1451
1451
|
let framework = {
|
|
1452
|
-
version: "0.7.
|
|
1452
|
+
version: "0.7.8",
|
|
1453
1453
|
poweredBy: "wooksjs",
|
|
1454
1454
|
link: "https://wooks.moost.org/",
|
|
1455
1455
|
image: "https://wooks.moost.org/wooks-full-logo.svg"
|
package/dist/index.mjs
CHANGED
|
@@ -1426,7 +1426,7 @@ function error_tl_default(ctx) {
|
|
|
1426
1426
|
//#endregion
|
|
1427
1427
|
//#region packages/event-http/src/response/wooks-http-response.ts
|
|
1428
1428
|
let framework = {
|
|
1429
|
-
version: "0.7.
|
|
1429
|
+
version: "0.7.8",
|
|
1430
1430
|
poweredBy: "wooksjs",
|
|
1431
1431
|
link: "https://wooks.moost.org/",
|
|
1432
1432
|
image: "https://wooks.moost.org/wooks-full-logo.svg"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wooksjs/event-http",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.9",
|
|
4
4
|
"description": "@wooksjs/event-http",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"api",
|
|
@@ -25,13 +25,8 @@
|
|
|
25
25
|
"url": "git+https://github.com/wooksjs/wooksjs.git",
|
|
26
26
|
"directory": "packages/event-http"
|
|
27
27
|
},
|
|
28
|
-
"bin": {
|
|
29
|
-
"wooksjs-event-http-skill": "scripts/setup-skills.js"
|
|
30
|
-
},
|
|
31
28
|
"files": [
|
|
32
|
-
"dist"
|
|
33
|
-
"skills",
|
|
34
|
-
"scripts"
|
|
29
|
+
"dist"
|
|
35
30
|
],
|
|
36
31
|
"main": "dist/index.cjs",
|
|
37
32
|
"module": "dist/index.mjs",
|
|
@@ -47,17 +42,16 @@
|
|
|
47
42
|
"devDependencies": {
|
|
48
43
|
"typescript": "^5.9.3",
|
|
49
44
|
"vitest": "^3.2.4",
|
|
50
|
-
"@wooksjs/event-core": "^0.7.
|
|
51
|
-
"wooks": "^0.7.
|
|
45
|
+
"@wooksjs/event-core": "^0.7.9",
|
|
46
|
+
"wooks": "^0.7.9"
|
|
52
47
|
},
|
|
53
48
|
"peerDependencies": {
|
|
54
49
|
"@prostojs/logger": "^0.4.3",
|
|
55
50
|
"@prostojs/router": "^0.3.3",
|
|
56
|
-
"@wooksjs/event-core": "^0.7.
|
|
57
|
-
"wooks": "^0.7.
|
|
51
|
+
"@wooksjs/event-core": "^0.7.9",
|
|
52
|
+
"wooks": "^0.7.9"
|
|
58
53
|
},
|
|
59
54
|
"scripts": {
|
|
60
|
-
"build": "rolldown -c ../../rolldown.config.mjs"
|
|
61
|
-
"setup-skills": "node scripts/setup-skills.js"
|
|
55
|
+
"build": "rolldown -c ../../rolldown.config.mjs"
|
|
62
56
|
}
|
|
63
57
|
}
|
package/scripts/setup-skills.js
DELETED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/* prettier-ignore */
|
|
3
|
-
'use strict'
|
|
4
|
-
|
|
5
|
-
const fs = require('fs')
|
|
6
|
-
const path = require('path')
|
|
7
|
-
const os = require('os')
|
|
8
|
-
|
|
9
|
-
const SKILL_NAME = 'wooksjs-event-http'
|
|
10
|
-
const SKILL_SRC = path.join(__dirname, '..', 'skills', SKILL_NAME)
|
|
11
|
-
|
|
12
|
-
if (!fs.existsSync(SKILL_SRC)) {
|
|
13
|
-
console.error(`No skills found at ${SKILL_SRC}`)
|
|
14
|
-
console.error('Add your SKILL.md files to the skills/' + SKILL_NAME + '/ directory first.')
|
|
15
|
-
process.exit(1)
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const AGENTS = {
|
|
19
|
-
'Claude Code': { dir: '.claude/skills', global: path.join(os.homedir(), '.claude', 'skills') },
|
|
20
|
-
'Cursor': { dir: '.cursor/skills', global: path.join(os.homedir(), '.cursor', 'skills') },
|
|
21
|
-
'Windsurf': { dir: '.windsurf/skills', global: path.join(os.homedir(), '.windsurf', 'skills') },
|
|
22
|
-
'Codex': { dir: '.codex/skills', global: path.join(os.homedir(), '.codex', 'skills') },
|
|
23
|
-
'OpenCode': { dir: '.opencode/skills', global: path.join(os.homedir(), '.opencode', 'skills') },
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const args = process.argv.slice(2)
|
|
27
|
-
const isGlobal = args.includes('--global') || args.includes('-g')
|
|
28
|
-
const isPostinstall = args.includes('--postinstall')
|
|
29
|
-
let installed = 0, skipped = 0
|
|
30
|
-
const installedDirs = []
|
|
31
|
-
|
|
32
|
-
for (const [agentName, cfg] of Object.entries(AGENTS)) {
|
|
33
|
-
const targetBase = isGlobal ? cfg.global : path.join(process.cwd(), cfg.dir)
|
|
34
|
-
const agentRootDir = path.dirname(cfg.global) // Check if the agent has ever been installed globally
|
|
35
|
-
|
|
36
|
-
// In postinstall mode: silently skip agents that aren't set up globally
|
|
37
|
-
if (isPostinstall || isGlobal) {
|
|
38
|
-
if (!fs.existsSync(agentRootDir)) { skipped++; continue }
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const dest = path.join(targetBase, SKILL_NAME)
|
|
42
|
-
try {
|
|
43
|
-
fs.mkdirSync(dest, { recursive: true })
|
|
44
|
-
fs.cpSync(SKILL_SRC, dest, { recursive: true })
|
|
45
|
-
console.log(`[${SKILL_NAME}] installed to ${dest}`)
|
|
46
|
-
installed++
|
|
47
|
-
if (!isGlobal) installedDirs.push(cfg.dir + '/' + SKILL_NAME)
|
|
48
|
-
} catch (err) {
|
|
49
|
-
console.warn(`[${SKILL_NAME}] failed — ${err.message}`)
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Add locally-installed skill dirs to .gitignore
|
|
54
|
-
if (!isGlobal && installedDirs.length > 0) {
|
|
55
|
-
const gitignorePath = path.join(process.cwd(), '.gitignore')
|
|
56
|
-
let gitignoreContent = ''
|
|
57
|
-
try { gitignoreContent = fs.readFileSync(gitignorePath, 'utf8') } catch {}
|
|
58
|
-
const linesToAdd = installedDirs.filter(d => !gitignoreContent.includes(d))
|
|
59
|
-
if (linesToAdd.length > 0) {
|
|
60
|
-
const hasHeader = gitignoreContent.includes('# AI agent skills')
|
|
61
|
-
const block = (gitignoreContent && !gitignoreContent.endsWith('\n') ? '\n' : '')
|
|
62
|
-
+ (hasHeader ? '' : '\n# AI agent skills (auto-generated by setup-skills)\n')
|
|
63
|
-
+ linesToAdd.join('\n') + '\n'
|
|
64
|
-
fs.appendFileSync(gitignorePath, block)
|
|
65
|
-
console.log(`[${SKILL_NAME}] added entries to .gitignore`)
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
if (installed === 0 && isPostinstall) {
|
|
70
|
-
// Silence is fine — no agents present, nothing to do
|
|
71
|
-
} else if (installed === 0 && skipped === Object.keys(AGENTS).length) {
|
|
72
|
-
console.log('No agent directories detected. Try --global or run without it for project-local install.')
|
|
73
|
-
} else if (installed === 0) {
|
|
74
|
-
console.log('Nothing installed. Run without --global to install project-locally.')
|
|
75
|
-
} else {
|
|
76
|
-
console.log(`Done! Restart your AI agent to pick up the "${SKILL_NAME}" skill.`)
|
|
77
|
-
}
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: wooksjs-event-http
|
|
3
|
-
description: Use this skill when working with @wooksjs/event-http — to create HTTP servers with createHttpApp(), register route handlers with app.get()/post()/put()/patch()/delete(), read request data with useRequest()/useHeaders()/useCookies()/useUrlParams()/useAuthorization()/useAccept(), control responses with useResponse() and HttpResponse (status, headers, cookies, cache control, streaming), throw HTTP errors with HttpError, test handlers with prepareTestHttpContext(), or integrate with existing Node.js servers via getServerCb().
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# @wooksjs/event-http
|
|
7
|
-
|
|
8
|
-
HTTP adapter for Wooks. Composable-based request handling where every piece of data is available on demand, typed, and cached per request. No middleware, no `req`/`res` parameters — just function calls.
|
|
9
|
-
|
|
10
|
-
## How to use this skill
|
|
11
|
-
|
|
12
|
-
Read the domain file that matches the task. Do not load all files — only what you need.
|
|
13
|
-
|
|
14
|
-
| Domain | File | Load when... |
|
|
15
|
-
| -------------------- | -------------------------- | -------------------------------------------------------------------------------------- |
|
|
16
|
-
| Core setup & routing | [core.md](core.md) | Creating an app, registering routes, starting a server, understanding the architecture |
|
|
17
|
-
| Request composables | [request.md](request.md) | Reading request data: headers, cookies, query params, body, authorization, IP |
|
|
18
|
-
| Response API | [response.md](response.md) | Setting status, headers, cookies, cache control, sending responses, error handling |
|
|
19
|
-
| Testing | [testing.md](testing.md) | Writing tests for handlers and composables with `prepareTestHttpContext` |
|
|
20
|
-
|
|
21
|
-
## Quick reference
|
|
22
|
-
|
|
23
|
-
```ts
|
|
24
|
-
import { createHttpApp } from '@wooksjs/event-http'
|
|
25
|
-
|
|
26
|
-
// Composables (no arguments — context via AsyncLocalStorage)
|
|
27
|
-
import {
|
|
28
|
-
useRequest,
|
|
29
|
-
useResponse,
|
|
30
|
-
useHeaders,
|
|
31
|
-
useCookies,
|
|
32
|
-
useUrlParams,
|
|
33
|
-
useAuthorization,
|
|
34
|
-
useAccept,
|
|
35
|
-
useRouteParams,
|
|
36
|
-
useLogger,
|
|
37
|
-
} from '@wooksjs/event-http'
|
|
38
|
-
|
|
39
|
-
// Errors
|
|
40
|
-
import { HttpError } from '@wooksjs/event-http'
|
|
41
|
-
|
|
42
|
-
// Testing
|
|
43
|
-
import { prepareTestHttpContext } from '@wooksjs/event-http'
|
|
44
|
-
```
|
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
# Core setup & routing — @wooksjs/event-http
|
|
2
|
-
|
|
3
|
-
> Creating an app, registering routes, server lifecycle, and architecture overview.
|
|
4
|
-
|
|
5
|
-
## Concepts
|
|
6
|
-
|
|
7
|
-
`@wooksjs/event-http` wraps a Node.js HTTP server with Wooks' composable architecture. Each incoming request gets its own `EventContext` (from `@wooksjs/event-core`), and handlers are plain functions that return their response. All request data is accessed through composables — on demand, typed, cached.
|
|
8
|
-
|
|
9
|
-
The adapter creates context, seeds it with the `IncomingMessage` and `HttpResponse`, looks up routes, and runs handlers. Handlers never receive `req`/`res` parameters.
|
|
10
|
-
|
|
11
|
-
## Installation
|
|
12
|
-
|
|
13
|
-
```bash
|
|
14
|
-
pnpm add @wooksjs/event-http
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
Peer dependencies: `@wooksjs/event-core`, `wooks`, `@prostojs/router`, `@prostojs/logger`.
|
|
18
|
-
|
|
19
|
-
## API Reference
|
|
20
|
-
|
|
21
|
-
### `createHttpApp(opts?): WooksHttp`
|
|
22
|
-
|
|
23
|
-
Creates and returns a `WooksHttp` instance.
|
|
24
|
-
|
|
25
|
-
```ts
|
|
26
|
-
import { createHttpApp } from '@wooksjs/event-http'
|
|
27
|
-
|
|
28
|
-
const app = createHttpApp()
|
|
29
|
-
app.get('/hello', () => 'Hello World!')
|
|
30
|
-
app.listen(3000)
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
**Options (`TWooksHttpOptions`):**
|
|
34
|
-
|
|
35
|
-
| Option | Type | Description |
|
|
36
|
-
| ---------------- | ------------------------------------ | ------------------------------------------------------------------------- |
|
|
37
|
-
| `logger` | `TConsoleBase` | Custom logger instance |
|
|
38
|
-
| `onNotFound` | `TWooksHandler` | Handler called when no route matches (default: 404 HttpError) |
|
|
39
|
-
| `router` | router options | Custom router configuration |
|
|
40
|
-
| `requestLimits` | `Omit<TRequestLimits, 'perRequest'>` | Default body size/timeout limits for all requests |
|
|
41
|
-
| `responseClass` | `typeof WooksHttpResponse` | Custom response subclass (default: `WooksHttpResponse`) |
|
|
42
|
-
| `defaultHeaders` | `Record<string, string \| string[]>` | Default headers applied to every response (e.g. from `securityHeaders()`) |
|
|
43
|
-
|
|
44
|
-
### Route registration
|
|
45
|
-
|
|
46
|
-
```ts
|
|
47
|
-
app.get(path, handler)
|
|
48
|
-
app.post(path, handler)
|
|
49
|
-
app.put(path, handler)
|
|
50
|
-
app.patch(path, handler)
|
|
51
|
-
app.delete(path, handler)
|
|
52
|
-
app.head(path, handler)
|
|
53
|
-
app.options(path, handler)
|
|
54
|
-
app.all(path, handler) // matches any HTTP method
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
Handlers are plain functions. Return value becomes the response body:
|
|
58
|
-
|
|
59
|
-
```ts
|
|
60
|
-
app.get('/users/:id', () => {
|
|
61
|
-
const { params } = useRouteParams<{ id: string }>()
|
|
62
|
-
return { id: params.id } // → 200 application/json
|
|
63
|
-
})
|
|
64
|
-
|
|
65
|
-
app.post('/users', async () => {
|
|
66
|
-
const { parseBody } = useBody() // from @wooksjs/http-body
|
|
67
|
-
const user = await parseBody<{ name: string }>()
|
|
68
|
-
return { created: user.name } // → 201 application/json (POST auto-status)
|
|
69
|
-
})
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
### Server lifecycle
|
|
73
|
-
|
|
74
|
-
```ts
|
|
75
|
-
// Start server
|
|
76
|
-
await app.listen(3000)
|
|
77
|
-
await app.listen(3000, '0.0.0.0')
|
|
78
|
-
|
|
79
|
-
// Use with existing server
|
|
80
|
-
import http from 'http'
|
|
81
|
-
const server = http.createServer(app.getServerCb())
|
|
82
|
-
server.listen(3000)
|
|
83
|
-
app.attachServer(server)
|
|
84
|
-
|
|
85
|
-
// Stop server
|
|
86
|
-
await app.close()
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
### `getServerCb()`
|
|
90
|
-
|
|
91
|
-
Returns a `(req, res) => void` callback for use with any Node.js HTTP server:
|
|
92
|
-
|
|
93
|
-
```ts
|
|
94
|
-
const cb = app.getServerCb()
|
|
95
|
-
http.createServer(cb).listen(3000)
|
|
96
|
-
// or with https, http2, etc.
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
## Routing
|
|
100
|
-
|
|
101
|
-
Built on [`@prostojs/router`](https://github.com/prostojs/router). Supports:
|
|
102
|
-
|
|
103
|
-
- Static routes: `/api/users`
|
|
104
|
-
- Parametric routes: `/users/:id`
|
|
105
|
-
- Wildcards: `/static/*`
|
|
106
|
-
- Multiple wildcards: `/static/*/assets/*`
|
|
107
|
-
- Regex constraints: `/api/time/:hours(\\d{2})h:minutes(\\d{2})m`
|
|
108
|
-
- Regex wildcards: `/static/*(\\d+)`
|
|
109
|
-
|
|
110
|
-
Route params accessed via `useRouteParams()`:
|
|
111
|
-
|
|
112
|
-
```ts
|
|
113
|
-
app.get('/users/:id', () => {
|
|
114
|
-
const { params } = useRouteParams<{ id: string }>()
|
|
115
|
-
return { userId: params.id }
|
|
116
|
-
})
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
## Auto-status
|
|
120
|
-
|
|
121
|
-
When no explicit status is set, it's inferred from the HTTP method and response body:
|
|
122
|
-
|
|
123
|
-
| Method | With body | Without body |
|
|
124
|
-
| ------ | ------------ | -------------- |
|
|
125
|
-
| GET | 200 OK | 204 No Content |
|
|
126
|
-
| POST | 201 Created | 204 No Content |
|
|
127
|
-
| PUT | 201 Created | 204 No Content |
|
|
128
|
-
| PATCH | 202 Accepted | 204 No Content |
|
|
129
|
-
| DELETE | 202 Accepted | 204 No Content |
|
|
130
|
-
|
|
131
|
-
## Handler chain
|
|
132
|
-
|
|
133
|
-
Multiple handlers can be registered for the same route. If one throws, the next is tried:
|
|
134
|
-
|
|
135
|
-
```ts
|
|
136
|
-
app.get('/resource', authHandler, mainHandler)
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
If all handlers throw, the last error is sent as the response.
|
|
140
|
-
|
|
141
|
-
## Best Practices
|
|
142
|
-
|
|
143
|
-
- Return values directly — the framework handles serialization and status codes
|
|
144
|
-
- Use `HttpError` for error responses: `throw new HttpError(404)` or `throw new HttpError(400, 'Invalid input')`
|
|
145
|
-
- Use `useResponse()` only when you need explicit control over headers, cookies, or status
|
|
146
|
-
- For `getServerCb()`, call `attachServer(server)` if you want `close()` to work
|
|
147
|
-
|
|
148
|
-
## Gotchas
|
|
149
|
-
|
|
150
|
-
- Handlers receive no arguments — all data comes from composables
|
|
151
|
-
- `listen()` returns a Promise — `await` it or handle rejection
|
|
152
|
-
- `getServerCb()` doesn't automatically attach the server — call `attachServer()` if needed
|
|
@@ -1,204 +0,0 @@
|
|
|
1
|
-
# Request composables — @wooksjs/event-http
|
|
2
|
-
|
|
3
|
-
> Reading request data: headers, cookies, query params, body, authorization, IP address.
|
|
4
|
-
|
|
5
|
-
## Concepts
|
|
6
|
-
|
|
7
|
-
All request data is accessed through composables — functions that resolve context via `AsyncLocalStorage`. They take no arguments (optionally accept `ctx` for performance). Data is parsed lazily on first access and cached for the request lifetime.
|
|
8
|
-
|
|
9
|
-
All composables are importable from `@wooksjs/event-http`.
|
|
10
|
-
|
|
11
|
-
## API Reference
|
|
12
|
-
|
|
13
|
-
### `useRequest(ctx?)`
|
|
14
|
-
|
|
15
|
-
The primary request composable. Returns method, URL, headers, raw body, IP, and request limit controls.
|
|
16
|
-
|
|
17
|
-
```ts
|
|
18
|
-
import { useRequest } from '@wooksjs/event-http'
|
|
19
|
-
|
|
20
|
-
const { method, url, headers, rawBody, getIp, reqId } = useRequest()
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
**Returned properties:**
|
|
24
|
-
|
|
25
|
-
| Property | Type | Description |
|
|
26
|
-
| ---------------- | ------------------------------------------------- | -------------------------------------------------- |
|
|
27
|
-
| `raw` | `IncomingMessage` | Node.js raw request object |
|
|
28
|
-
| `url` | `string` | Request URL |
|
|
29
|
-
| `method` | `string` | HTTP method |
|
|
30
|
-
| `headers` | `IncomingHttpHeaders` | Request headers |
|
|
31
|
-
| `rawBody` | `() => Promise<Buffer>` | Lazy — reads and decompresses request body on call |
|
|
32
|
-
| `reqId` | `() => string` | Lazy UUID per request |
|
|
33
|
-
| `getIp(opts?)` | `(opts?: { trustProxy: boolean }) => string` | Client IP (with optional proxy trust) |
|
|
34
|
-
| `getIpList()` | `() => { remoteIp: string; forwarded: string[] }` | All IPs (remote + X-Forwarded-For) |
|
|
35
|
-
| `isCompressed()` | `() => boolean` | Whether the request body is compressed |
|
|
36
|
-
|
|
37
|
-
**Request limits (per-request override):**
|
|
38
|
-
|
|
39
|
-
```ts
|
|
40
|
-
const { setMaxCompressed, setMaxInflated, setMaxRatio, setReadTimeoutMs } = useRequest()
|
|
41
|
-
setMaxCompressed(5 * 1024 * 1024) // 5 MB
|
|
42
|
-
setReadTimeoutMs(30_000) // 30 seconds
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
Default limits: `maxCompressed: 1MB`, `maxInflated: 10MB`, `maxRatio: 100`, `readTimeoutMs: 10s`.
|
|
46
|
-
|
|
47
|
-
### `useHeaders(ctx?): IncomingHttpHeaders`
|
|
48
|
-
|
|
49
|
-
Returns request headers directly. Shorthand for `useRequest().headers`.
|
|
50
|
-
|
|
51
|
-
```ts
|
|
52
|
-
import { useHeaders } from '@wooksjs/event-http'
|
|
53
|
-
|
|
54
|
-
const { host, authorization, 'content-type': contentType } = useHeaders()
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
### `useCookies(ctx?)`
|
|
58
|
-
|
|
59
|
-
Parses incoming request cookies lazily (per cookie name, via `cachedBy`).
|
|
60
|
-
|
|
61
|
-
```ts
|
|
62
|
-
import { useCookies } from '@wooksjs/event-http'
|
|
63
|
-
|
|
64
|
-
const { getCookie, raw } = useCookies()
|
|
65
|
-
const session = getCookie('session_id') // parsed + cached
|
|
66
|
-
const theme = getCookie('theme') // parsed + cached (different key)
|
|
67
|
-
// raw = raw Cookie header string
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
### `useUrlParams(ctx?)`
|
|
71
|
-
|
|
72
|
-
Provides access to URL query parameters.
|
|
73
|
-
|
|
74
|
-
```ts
|
|
75
|
-
import { useUrlParams } from '@wooksjs/event-http'
|
|
76
|
-
|
|
77
|
-
const { params, toJson, raw } = useUrlParams()
|
|
78
|
-
|
|
79
|
-
// URLSearchParams API
|
|
80
|
-
const page = params().get('page')
|
|
81
|
-
const tags = params().getAll('tag')
|
|
82
|
-
|
|
83
|
-
// As a plain object
|
|
84
|
-
const query = toJson() // { page: '1', tag: ['a', 'b'] }
|
|
85
|
-
|
|
86
|
-
// Raw query string
|
|
87
|
-
const rawQuery = raw() // '?page=1&tag=a&tag=b'
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
### `useAuthorization(ctx?)`
|
|
91
|
-
|
|
92
|
-
Parses the Authorization header (supports Basic and Bearer).
|
|
93
|
-
|
|
94
|
-
```ts
|
|
95
|
-
import { useAuthorization } from '@wooksjs/event-http'
|
|
96
|
-
|
|
97
|
-
const { authorization, type, credentials, is, basicCredentials } = useAuthorization()
|
|
98
|
-
|
|
99
|
-
if (is('bearer')) {
|
|
100
|
-
const token = credentials() // the raw token string
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
if (is('basic')) {
|
|
104
|
-
const { username, password } = basicCredentials()!
|
|
105
|
-
}
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
**Returned properties:**
|
|
109
|
-
|
|
110
|
-
| Property | Type | Description |
|
|
111
|
-
| -------------------- | -------------------------------- | -------------------------------------------------------------- |
|
|
112
|
-
| `authorization` | `string \| undefined` | Raw Authorization header value |
|
|
113
|
-
| `type()` | `string \| null` | Auth scheme: `'Basic'`, `'Bearer'`, etc. |
|
|
114
|
-
| `credentials()` | `string \| null` | Everything after the scheme |
|
|
115
|
-
| `is(type)` | `boolean` | Check auth scheme: `'basic'`, `'bearer'`, or any custom scheme |
|
|
116
|
-
| `basicCredentials()` | `{ username, password } \| null` | Decoded Basic credentials |
|
|
117
|
-
|
|
118
|
-
### `useAccept(ctx?)`
|
|
119
|
-
|
|
120
|
-
Checks the request's `Accept` header for MIME type support. Uses short names (`'json'`, `'html'`, `'xml'`, `'text'`) or full MIME types.
|
|
121
|
-
|
|
122
|
-
```ts
|
|
123
|
-
import { useAccept } from '@wooksjs/event-http'
|
|
124
|
-
|
|
125
|
-
const { accept, has } = useAccept()
|
|
126
|
-
|
|
127
|
-
if (has('json')) {
|
|
128
|
-
/* ... */
|
|
129
|
-
}
|
|
130
|
-
if (has('image/png')) {
|
|
131
|
-
/* ... */
|
|
132
|
-
}
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
### `useRouteParams<T>(ctx?)`
|
|
136
|
-
|
|
137
|
-
Route parameters from the URL. Re-exported from `@wooksjs/event-core`.
|
|
138
|
-
|
|
139
|
-
```ts
|
|
140
|
-
import { useRouteParams } from '@wooksjs/event-http'
|
|
141
|
-
|
|
142
|
-
// Given route: /users/:id/posts/:postId
|
|
143
|
-
const { params, get } = useRouteParams<{ id: string; postId: string }>()
|
|
144
|
-
params.id // '42'
|
|
145
|
-
get('postId') // '7'
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
### `useLogger(ctx?): Logger`
|
|
149
|
-
|
|
150
|
-
Logger from the current event context. Re-exported from `@wooksjs/event-core`.
|
|
151
|
-
|
|
152
|
-
```ts
|
|
153
|
-
import { useLogger } from '@wooksjs/event-http'
|
|
154
|
-
|
|
155
|
-
const log = useLogger()
|
|
156
|
-
log.info('handling request')
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
## Common Patterns
|
|
160
|
-
|
|
161
|
-
### Pattern: Auth guard with early return
|
|
162
|
-
|
|
163
|
-
```ts
|
|
164
|
-
app.post('/admin/action', async () => {
|
|
165
|
-
const { is, credentials } = useAuthorization()
|
|
166
|
-
if (!is('bearer')) throw new HttpError(401)
|
|
167
|
-
|
|
168
|
-
const token = credentials()!
|
|
169
|
-
const user = await verifyToken(token)
|
|
170
|
-
if (!user.isAdmin) throw new HttpError(403)
|
|
171
|
-
|
|
172
|
-
// Body is never parsed if auth fails
|
|
173
|
-
const { parseBody } = useBody() // from @wooksjs/http-body
|
|
174
|
-
return parseBody()
|
|
175
|
-
})
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
### Pattern: Performance — resolve context once
|
|
179
|
-
|
|
180
|
-
```ts
|
|
181
|
-
import { current } from '@wooksjs/event-core'
|
|
182
|
-
|
|
183
|
-
app.get('/hot-path', () => {
|
|
184
|
-
const ctx = current()
|
|
185
|
-
const { method } = useRequest(ctx)
|
|
186
|
-
const { getCookie } = useCookies(ctx)
|
|
187
|
-
const { params } = useUrlParams(ctx)
|
|
188
|
-
// 1 ALS lookup instead of 3
|
|
189
|
-
})
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
## Best Practices
|
|
193
|
-
|
|
194
|
-
- Composables are lazy — call them only when you need the data
|
|
195
|
-
- Pass `ctx` explicitly in hot paths with multiple composable calls
|
|
196
|
-
- Use `getCookie(name)` over parsing all cookies when you need only a few
|
|
197
|
-
- `rawBody()` handles decompression (gzip, deflate, brotli) automatically with limits enforcement
|
|
198
|
-
|
|
199
|
-
## Gotchas
|
|
200
|
-
|
|
201
|
-
- `rawBody()` returns a `Promise<Buffer>` — always `await` it
|
|
202
|
-
- `rawBody()` consumes the stream — it's cached, so second call returns the same buffer
|
|
203
|
-
- `getCookie()` returns `null` (not `undefined`) when a cookie doesn't exist
|
|
204
|
-
- `useRequest()` limits setters use copy-on-write — they don't affect other requests
|
|
@@ -1,267 +0,0 @@
|
|
|
1
|
-
# Response API — @wooksjs/event-http
|
|
2
|
-
|
|
3
|
-
> Status, headers, cookies, cache control, error handling, and response sending.
|
|
4
|
-
|
|
5
|
-
## Concepts
|
|
6
|
-
|
|
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
|
-
|
|
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
|
-
|
|
11
|
-
## API Reference
|
|
12
|
-
|
|
13
|
-
### `useResponse(ctx?): HttpResponse`
|
|
14
|
-
|
|
15
|
-
Returns the `HttpResponse` for the current request.
|
|
16
|
-
|
|
17
|
-
```ts
|
|
18
|
-
import { useResponse } from '@wooksjs/event-http'
|
|
19
|
-
|
|
20
|
-
const response = useResponse()
|
|
21
|
-
response.setStatus(200).setHeader('x-custom', 'value')
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
### Status
|
|
25
|
-
|
|
26
|
-
```ts
|
|
27
|
-
response.status = 201 // set via property
|
|
28
|
-
response.setStatus(201) // set via method (chainable)
|
|
29
|
-
const code = response.status // get current status
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
If not set explicitly, status is inferred automatically (see core.md Auto-status).
|
|
33
|
-
|
|
34
|
-
### Headers
|
|
35
|
-
|
|
36
|
-
```ts
|
|
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)
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
### Cookies (outgoing)
|
|
49
|
-
|
|
50
|
-
Set-Cookie headers are managed via `HttpResponse`:
|
|
51
|
-
|
|
52
|
-
```ts
|
|
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
|
-
})
|
|
62
|
-
|
|
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
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
**`TCookieAttributes`:**
|
|
70
|
-
|
|
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 |
|
|
80
|
-
|
|
81
|
-
### Cache control
|
|
82
|
-
|
|
83
|
-
```ts
|
|
84
|
-
response.setCacheControl({
|
|
85
|
-
public: true,
|
|
86
|
-
maxAge: 3600,
|
|
87
|
-
sMaxage: 7200,
|
|
88
|
-
noStore: false,
|
|
89
|
-
noCache: false,
|
|
90
|
-
mustRevalidate: true,
|
|
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
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
### Body and content type inference
|
|
100
|
-
|
|
101
|
-
Return values are automatically serialized:
|
|
102
|
-
|
|
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 |
|
|
112
|
-
|
|
113
|
-
### Raw response access
|
|
114
|
-
|
|
115
|
-
For escape hatches (SSE, WebSocket upgrades, etc.):
|
|
116
|
-
|
|
117
|
-
```ts
|
|
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')
|
|
122
|
-
|
|
123
|
-
// Passthrough — you write to res, but framework still finalizes
|
|
124
|
-
const res = response.getRawRes(true)
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
### `responded` property
|
|
128
|
-
|
|
129
|
-
```ts
|
|
130
|
-
if (response.responded) {
|
|
131
|
-
// Response already sent — don't try to send again
|
|
132
|
-
}
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
## HttpError
|
|
136
|
-
|
|
137
|
-
For error responses, throw `HttpError`:
|
|
138
|
-
|
|
139
|
-
```ts
|
|
140
|
-
import { HttpError } from '@wooksjs/event-http'
|
|
141
|
-
|
|
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
|
-
```
|
|
146
|
-
|
|
147
|
-
`HttpError` skips stack trace capture for performance (these are expected control-flow errors).
|
|
148
|
-
|
|
149
|
-
**Error rendering (`WooksHttpResponse`):**
|
|
150
|
-
|
|
151
|
-
The default response class renders errors based on the `Accept` header:
|
|
152
|
-
|
|
153
|
-
- `application/json` → JSON `{ statusCode, message, error }`
|
|
154
|
-
- `text/html` → Styled HTML error page with SVG icons
|
|
155
|
-
- `text/plain` → Plain text error
|
|
156
|
-
|
|
157
|
-
Override by providing a custom `responseClass` to `createHttpApp()`:
|
|
158
|
-
|
|
159
|
-
```ts
|
|
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
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
const app = createHttpApp({ responseClass: MyResponse })
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
### Default headers and security headers
|
|
172
|
-
|
|
173
|
-
Pre-populate response headers for every request via `defaultHeaders`:
|
|
174
|
-
|
|
175
|
-
```ts
|
|
176
|
-
import { createHttpApp, securityHeaders } from '@wooksjs/event-http'
|
|
177
|
-
|
|
178
|
-
const app = createHttpApp({ defaultHeaders: securityHeaders() })
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
`securityHeaders(opts?)` returns recommended HTTP security headers:
|
|
182
|
-
|
|
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` |
|
|
191
|
-
|
|
192
|
-
Options: `string` (override) or `false` (disable). `strictTransportSecurity` is opt-in only.
|
|
193
|
-
|
|
194
|
-
```ts
|
|
195
|
-
securityHeaders({
|
|
196
|
-
contentSecurityPolicy: false,
|
|
197
|
-
referrerPolicy: 'strict-origin-when-cross-origin',
|
|
198
|
-
strictTransportSecurity: 'max-age=31536000; includeSubDomains',
|
|
199
|
-
})
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
Per-endpoint: `response.setHeaders(securityHeaders({ ... }))`.
|
|
203
|
-
|
|
204
|
-
## Common Patterns
|
|
205
|
-
|
|
206
|
-
### Pattern: Full response control
|
|
207
|
-
|
|
208
|
-
```ts
|
|
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' }
|
|
217
|
-
})
|
|
218
|
-
```
|
|
219
|
-
|
|
220
|
-
### Pattern: Redirect
|
|
221
|
-
|
|
222
|
-
```ts
|
|
223
|
-
app.get('/old-path', () => {
|
|
224
|
-
useResponse().setStatus(301).setHeader('location', '/new-path')
|
|
225
|
-
})
|
|
226
|
-
```
|
|
227
|
-
|
|
228
|
-
### Pattern: Streaming response
|
|
229
|
-
|
|
230
|
-
```ts
|
|
231
|
-
import { createReadStream } from 'fs'
|
|
232
|
-
|
|
233
|
-
app.get('/file', () => {
|
|
234
|
-
useResponse().setContentType('application/octet-stream')
|
|
235
|
-
return createReadStream('/path/to/file')
|
|
236
|
-
})
|
|
237
|
-
```
|
|
238
|
-
|
|
239
|
-
### Pattern: Server-Sent Events
|
|
240
|
-
|
|
241
|
-
```ts
|
|
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"
|
|
251
|
-
})
|
|
252
|
-
```
|
|
253
|
-
|
|
254
|
-
## Best Practices
|
|
255
|
-
|
|
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()`
|
|
260
|
-
|
|
261
|
-
## Gotchas
|
|
262
|
-
|
|
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
|
|
@@ -1,150 +0,0 @@
|
|
|
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 { is } = useAuthorization()
|
|
44
|
-
expect(method).toBe('GET')
|
|
45
|
-
expect(params.id).toBe('42')
|
|
46
|
-
expect(is('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
|