@fastify/reply-from 12.6.2 → 12.6.4
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/.github/dependabot.yml +42 -2
- package/CLAUDE.md +66 -0
- package/README.md +3 -3
- package/index.js +14 -5
- package/package.json +6 -7
- package/test/fix-GHSA-v574-6498-x57v.test.js +219 -0
- package/test/http1-aborted-request.test.js +61 -0
- package/test/incomplete-request.test.js +142 -0
- package/types/index.tst.ts +123 -0
- package/types/index.test-d.ts +0 -194
package/.github/dependabot.yml
CHANGED
|
@@ -2,12 +2,52 @@ version: 2
|
|
|
2
2
|
updates:
|
|
3
3
|
- package-ecosystem: "github-actions"
|
|
4
4
|
directory: "/"
|
|
5
|
+
commit-message:
|
|
6
|
+
# Prefix all commit messages with "chore: "
|
|
7
|
+
prefix: "chore"
|
|
5
8
|
schedule:
|
|
6
|
-
interval: "
|
|
9
|
+
interval: "weekly"
|
|
10
|
+
cooldown:
|
|
11
|
+
default-days: 7
|
|
12
|
+
allow:
|
|
13
|
+
- dependency-name: "*"
|
|
14
|
+
update-types:
|
|
15
|
+
- "version-update:semver-major"
|
|
7
16
|
open-pull-requests-limit: 10
|
|
8
17
|
|
|
9
18
|
- package-ecosystem: "npm"
|
|
10
19
|
directory: "/"
|
|
20
|
+
commit-message:
|
|
21
|
+
# Prefix all commit messages with "chore: "
|
|
22
|
+
prefix: "chore"
|
|
11
23
|
schedule:
|
|
12
|
-
interval: "
|
|
24
|
+
interval: "weekly"
|
|
25
|
+
cooldown:
|
|
26
|
+
default-days: 7
|
|
27
|
+
versioning-strategy: "increase-if-necessary"
|
|
28
|
+
allow:
|
|
29
|
+
- dependency-name: "*"
|
|
30
|
+
update-types:
|
|
31
|
+
- "version-update:semver-major"
|
|
32
|
+
ignore:
|
|
33
|
+
# TODO: remove ignore until neostandard support ESLint 10
|
|
34
|
+
- dependency-name: "eslint"
|
|
35
|
+
- dependency-name: "neostandard"
|
|
36
|
+
- dependency-name: "@stylistic/*"
|
|
13
37
|
open-pull-requests-limit: 10
|
|
38
|
+
groups:
|
|
39
|
+
# Production dependencies with breaking changes
|
|
40
|
+
dependencies:
|
|
41
|
+
dependency-type: "production"
|
|
42
|
+
# ESLint related dependencies
|
|
43
|
+
dev-dependencies-eslint:
|
|
44
|
+
patterns:
|
|
45
|
+
- "eslint"
|
|
46
|
+
- "neostandard"
|
|
47
|
+
- "@stylistic/*"
|
|
48
|
+
# TypeScript related dependencies
|
|
49
|
+
dev-dependencies-typescript:
|
|
50
|
+
patterns:
|
|
51
|
+
- "@types/*"
|
|
52
|
+
- "tstyche"
|
|
53
|
+
- "typescript"
|
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## Development Commands
|
|
6
|
+
|
|
7
|
+
### Testing
|
|
8
|
+
- `npm test` - Run all tests (unit + TypeScript)
|
|
9
|
+
- `npm run test:unit` - Run unit tests with coverage using c8 and Node.js test runner
|
|
10
|
+
- `npm run test:typescript` - Run TypeScript definition tests using tsd
|
|
11
|
+
|
|
12
|
+
### Linting
|
|
13
|
+
- `npm run lint` - Run ESLint to check code style
|
|
14
|
+
- `npm run lint:fix` - Run ESLint with automatic fixes
|
|
15
|
+
- ESLint configuration uses neostandard with TypeScript support
|
|
16
|
+
|
|
17
|
+
### Building
|
|
18
|
+
- No build step required - this is a CommonJS library
|
|
19
|
+
|
|
20
|
+
## Architecture Overview
|
|
21
|
+
|
|
22
|
+
This is a Fastify plugin for proxying HTTP requests to upstream servers. The plugin decorates the Fastify reply with a `from()` method.
|
|
23
|
+
|
|
24
|
+
### Key Components
|
|
25
|
+
|
|
26
|
+
**Main Plugin (`index.js`)**
|
|
27
|
+
- Entry point that registers the plugin using fastify-plugin
|
|
28
|
+
- Decorates `reply.from(source, opts)` method
|
|
29
|
+
- Handles request proxying, retries, caching, and error handling
|
|
30
|
+
- Supports HTTP/1.1, HTTP/2, and Undici client strategies
|
|
31
|
+
|
|
32
|
+
**Request Builder (`lib/request.js`)**
|
|
33
|
+
- Factory for creating request handlers based on transport (HTTP/1.1, HTTP/2, Undici)
|
|
34
|
+
- Handles different agent configurations (HTTP, HTTPS, Unix sockets)
|
|
35
|
+
- Manages timeouts, connection pooling, and retry logic
|
|
36
|
+
- Returns unified interface: `{ request, close, retryOnError }`
|
|
37
|
+
|
|
38
|
+
**Utilities (`lib/utils.js`)**
|
|
39
|
+
- `filterPseudoHeaders()` - Removes HTTP/2 pseudo-headers (starting with :)
|
|
40
|
+
- `copyHeaders()` - Copies headers to Fastify reply
|
|
41
|
+
- `stripHttp1ConnectionHeaders()` - Removes HTTP/1 connection-specific headers for HTTP/2
|
|
42
|
+
- `buildURL()` - Constructs destination URLs with base path validation
|
|
43
|
+
|
|
44
|
+
**Error Definitions (`lib/errors.js`)**
|
|
45
|
+
- Standardized error types using @fastify/error
|
|
46
|
+
- Maps different error conditions to appropriate HTTP status codes
|
|
47
|
+
- Includes timeout, connection reset, and gateway errors
|
|
48
|
+
|
|
49
|
+
### Transport Strategies
|
|
50
|
+
|
|
51
|
+
1. **Undici (default)** - High-performance HTTP/1.1 client with connection pooling
|
|
52
|
+
2. **HTTP/2** - Native Node.js HTTP/2 client for modern protocols
|
|
53
|
+
3. **HTTP/1.1** - Node.js built-in http/https modules with custom agents
|
|
54
|
+
|
|
55
|
+
### Key Features
|
|
56
|
+
|
|
57
|
+
- **Caching**: URL parsing cache using toad-cache LruMap (default 100 entries)
|
|
58
|
+
- **Retries**: Configurable retry logic for socket hangups and 503 errors
|
|
59
|
+
- **Headers**: Smart header rewriting for HTTP/1.1 ↔ HTTP/2 compatibility
|
|
60
|
+
- **Body handling**: Supports streams, JSON encoding, and custom content types
|
|
61
|
+
- **Unix sockets**: Special handling for unix+http: and unix+https: protocols
|
|
62
|
+
- **Timeouts**: Per-request and session-level timeout configuration
|
|
63
|
+
|
|
64
|
+
### Configuration
|
|
65
|
+
|
|
66
|
+
The plugin accepts extensive configuration for different transport methods, retry behavior, caching, and request/response transformation hooks. See README.md for full option documentation.
|
package/README.md
CHANGED
|
@@ -70,7 +70,7 @@ Set the base URL for all the forwarded requests.
|
|
|
70
70
|
*String or String[]*:
|
|
71
71
|
|
|
72
72
|
* **Single string** → a normal `undici.Pool` / `http.request` client is used.
|
|
73
|
-
* **Array with ≥ 2 elements** → **[`undici.BalancedPool`](https://undici.nodejs.org
|
|
73
|
+
* **Array with ≥ 2 elements** → **[`undici.BalancedPool`](https://undici.nodejs.org/api/BalancedPool)** is automatically selected and requests are load-balanced round-robin across the given origins.
|
|
74
74
|
|
|
75
75
|
When you provide an array, only the *origin* (`protocol://host:port`) part of each URL is considered; any path component is ignored.
|
|
76
76
|
|
|
@@ -354,7 +354,7 @@ const customRetryLogic = ({req, res, err, getDefaultRetry}: RetryDetails) => {
|
|
|
354
354
|
### `reply.from(source, [opts])`
|
|
355
355
|
|
|
356
356
|
The plugin decorates the
|
|
357
|
-
[`Reply`](https://fastify.dev/docs/latest/Reference/Reply)
|
|
357
|
+
[`Reply`](https://fastify.dev/docs/latest/Reference/Reply/)
|
|
358
358
|
instance with a `from` method, which will reply to the original request
|
|
359
359
|
__from the desired source__. The options allows overrides of any part of
|
|
360
360
|
the request or response being sent or received to/from the source.
|
|
@@ -512,7 +512,7 @@ This library has:
|
|
|
512
512
|
- The default value for `requestTimeout` is 10 seconds (`10000`), a value of 0 disables the timeout.
|
|
513
513
|
- The default value for `sessionTimeout` is 60 seconds (`60000`), a value of 0 disables the timeout.
|
|
514
514
|
|
|
515
|
-
When a timeout happens, [`504 Gateway Timeout`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/504)
|
|
515
|
+
When a timeout happens, [`504 Gateway Timeout`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/504)
|
|
516
516
|
will be returned to the client.
|
|
517
517
|
|
|
518
518
|
## Compatibility with @fastify/multipart
|
package/index.js
CHANGED
|
@@ -81,7 +81,7 @@ const fastifyReplyFrom = fp(function from (fastify, opts, next) {
|
|
|
81
81
|
const dest = getUpstream(this.request, base)
|
|
82
82
|
let url
|
|
83
83
|
if (cache) {
|
|
84
|
-
const cacheKey = dest
|
|
84
|
+
const cacheKey = JSON.stringify([dest, source])
|
|
85
85
|
url = cache.get(cacheKey) || buildURL(source, dest)
|
|
86
86
|
cache.set(cacheKey, url)
|
|
87
87
|
} else {
|
|
@@ -146,7 +146,7 @@ const fastifyReplyFrom = fp(function from (fastify, opts, next) {
|
|
|
146
146
|
}
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
-
// according to https://
|
|
149
|
+
// according to https://datatracker.ietf.org/doc/html/rfc2616#section-4.3
|
|
150
150
|
// fastify ignore message body when it's a GET or HEAD request
|
|
151
151
|
// when proxy this request, we should reset the content-length to make it a valid http request
|
|
152
152
|
// discussion: https://github.com/fastify/fastify/issues/953
|
|
@@ -218,6 +218,11 @@ const fastifyReplyFrom = fp(function from (fastify, opts, next) {
|
|
|
218
218
|
)
|
|
219
219
|
} else {
|
|
220
220
|
copyHeaders(rewriteHeaders(res.headers, this.request), this)
|
|
221
|
+
// An HTTP/1 connection cannot be reused until its request body has
|
|
222
|
+
// been consumed. Close it if the upstream responds before that point.
|
|
223
|
+
if (!req.complete) {
|
|
224
|
+
this.header('connection', 'close')
|
|
225
|
+
}
|
|
221
226
|
}
|
|
222
227
|
try {
|
|
223
228
|
this.code(res.statusCode)
|
|
@@ -226,10 +231,14 @@ const fastifyReplyFrom = fp(function from (fastify, opts, next) {
|
|
|
226
231
|
onError(this, { error: new BadGatewayError() })
|
|
227
232
|
this.request.log.warn(err, 'response has invalid status code')
|
|
228
233
|
}
|
|
229
|
-
if (this.request.raw.aborted
|
|
234
|
+
if (this.request.raw.aborted) {
|
|
230
235
|
// the request could have been canceled before we got a response from the target
|
|
231
|
-
//
|
|
232
|
-
|
|
236
|
+
// clean up the upstream stream to prevent leaks and skip sending a reply
|
|
237
|
+
if (isHttp2) {
|
|
238
|
+
res.stream.close(NGHTTP2_CANCEL)
|
|
239
|
+
} else {
|
|
240
|
+
res.stream.destroy()
|
|
241
|
+
}
|
|
233
242
|
// no need to send a reply for aborted requests or call the onResponse callback
|
|
234
243
|
return
|
|
235
244
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fastify/reply-from",
|
|
3
|
-
"version": "12.6.
|
|
3
|
+
"version": "12.6.4",
|
|
4
4
|
"description": "forward your HTTP request to another server, for fastify",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"lint:fix": "eslint --fix",
|
|
11
11
|
"test": "npm run test:unit && npm run test:typescript",
|
|
12
12
|
"test:unit": "c8 node --test --test-timeout=30000",
|
|
13
|
-
"test:typescript": "
|
|
13
|
+
"test:typescript": "tstyche"
|
|
14
14
|
},
|
|
15
15
|
"repository": {
|
|
16
16
|
"type": "git",
|
|
@@ -59,10 +59,9 @@
|
|
|
59
59
|
],
|
|
60
60
|
"devDependencies": {
|
|
61
61
|
"@fastify/formbody": "^8.0.0",
|
|
62
|
-
"@fastify/multipart": "^
|
|
62
|
+
"@fastify/multipart": "^10.0.0",
|
|
63
63
|
"@sinonjs/fake-timers": "^15.0.0",
|
|
64
|
-
"@types/node": "^
|
|
65
|
-
"@types/tap": "^18.0.0",
|
|
64
|
+
"@types/node": "^26.0.0",
|
|
66
65
|
"c8": "^11.0.0",
|
|
67
66
|
"eslint": "^9.17.0",
|
|
68
67
|
"fastify": "^5.0.0",
|
|
@@ -73,14 +72,14 @@
|
|
|
73
72
|
"proxy": "^4.0.0",
|
|
74
73
|
"proxyquire": "^2.1.3",
|
|
75
74
|
"split2": "^4.2.0",
|
|
76
|
-
"
|
|
75
|
+
"tstyche": "^7.0.0"
|
|
77
76
|
},
|
|
78
77
|
"dependencies": {
|
|
79
78
|
"@fastify/error": "^4.0.0",
|
|
80
79
|
"end-of-stream": "^1.4.4",
|
|
81
80
|
"fast-content-type-parse": "^3.0.0",
|
|
82
81
|
"fast-querystring": "^1.1.2",
|
|
83
|
-
"fastify-plugin": "^
|
|
82
|
+
"fastify-plugin": "^6.0.0",
|
|
84
83
|
"toad-cache": "^3.7.0",
|
|
85
84
|
"undici": "^7.0.0"
|
|
86
85
|
},
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const t = require('node:test')
|
|
4
|
+
const Fastify = require('fastify')
|
|
5
|
+
const From = require('..')
|
|
6
|
+
|
|
7
|
+
// GHSA-v574-6498-x57v: Cache key collision via ambiguous concatenation.
|
|
8
|
+
// dest + source was used as the cache key. Different (dest, source) pairs
|
|
9
|
+
// could produce identical concatenations while resolving to different URLs,
|
|
10
|
+
// causing a request to be forwarded to the wrong upstream.
|
|
11
|
+
//
|
|
12
|
+
// dest = http://127.0.0.1:31001 , source = /private
|
|
13
|
+
// dest = http://127.0.0.1:3100 , source = 1/private
|
|
14
|
+
//
|
|
15
|
+
// Both concatenate to "http://127.0.0.1:31001/private" but resolve to
|
|
16
|
+
// different URLs.
|
|
17
|
+
|
|
18
|
+
async function run (t) {
|
|
19
|
+
// victim upstream on port 31001 with a private route and a writable route
|
|
20
|
+
const victim = Fastify({ keepAliveTimeout: 1 })
|
|
21
|
+
victim.get('/private', async () => ({ marker: 'VICTIM_PRIVATE_DATA' }))
|
|
22
|
+
victim.get('/state', async () => ({ written: victim.written }))
|
|
23
|
+
victim.post('/private', async (req, reply) => {
|
|
24
|
+
victim.written = req.body
|
|
25
|
+
return { marker: 'VICTIM_PRIVATE_DATA', written: victim.written }
|
|
26
|
+
})
|
|
27
|
+
t.after(() => victim.close())
|
|
28
|
+
await victim.listen({ port: 31001 })
|
|
29
|
+
|
|
30
|
+
// attacker upstream on port 3100
|
|
31
|
+
const attacker = Fastify({ keepAliveTimeout: 1 })
|
|
32
|
+
attacker.get('/1/private', async () => ({ marker: 'ATTACKER_UPSTREAM' }))
|
|
33
|
+
attacker.post('/1/private', async (req, reply) => {
|
|
34
|
+
return { marker: 'ATTACKER_UPSTREAM', written: req.body }
|
|
35
|
+
})
|
|
36
|
+
t.after(() => attacker.close())
|
|
37
|
+
await attacker.listen({ port: 3100 })
|
|
38
|
+
|
|
39
|
+
// proxy using getUpstream() and default cache
|
|
40
|
+
const proxy = Fastify({ keepAliveTimeout: 1 })
|
|
41
|
+
proxy.register(From, {
|
|
42
|
+
base: 'http://localhost',
|
|
43
|
+
http: true
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
proxy.get('/proxy/victim/private', (req, reply) => {
|
|
47
|
+
reply.from('/private', {
|
|
48
|
+
getUpstream () {
|
|
49
|
+
return 'http://127.0.0.1:31001'
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
proxy.post('/proxy/victim/write', (req, reply) => {
|
|
55
|
+
reply.from('/private', {
|
|
56
|
+
method: 'POST',
|
|
57
|
+
body: JSON.stringify(req.body),
|
|
58
|
+
contentType: 'application/json',
|
|
59
|
+
getUpstream () {
|
|
60
|
+
return 'http://127.0.0.1:31001'
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
proxy.get('/proxy/attacker/private', (req, reply) => {
|
|
66
|
+
reply.from('1/private', {
|
|
67
|
+
getUpstream () {
|
|
68
|
+
return 'http://127.0.0.1:3100'
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
proxy.post('/proxy/attacker/write', (req, reply) => {
|
|
74
|
+
reply.from('1/private', {
|
|
75
|
+
method: 'POST',
|
|
76
|
+
body: JSON.stringify(req.body),
|
|
77
|
+
contentType: 'application/json',
|
|
78
|
+
getUpstream () {
|
|
79
|
+
return 'http://127.0.0.1:3100'
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
proxy.get('/proxy/victim/state', (req, reply) => {
|
|
85
|
+
reply.from('/state', {
|
|
86
|
+
getUpstream () {
|
|
87
|
+
return 'http://127.0.0.1:31001'
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
t.after(() => proxy.close())
|
|
93
|
+
await proxy.listen({ port: 31000 })
|
|
94
|
+
|
|
95
|
+
// prime the cache with the victim entry (dest = "http://127.0.0.1:31001", source = "/private")
|
|
96
|
+
const primeRes = await fetch('http://127.0.0.1:31000/proxy/victim/private')
|
|
97
|
+
t.assert.strictEqual(primeRes.status, 200)
|
|
98
|
+
|
|
99
|
+
// now the attacker pair (dest = "http://127.0.0.1:3100", source = "1/private")
|
|
100
|
+
// should NOT reuse the cached URL — it must resolve to the attacker upstream.
|
|
101
|
+
|
|
102
|
+
// 1. attacker read — should return ATTACKER_UPSTREAM, not VICTIM_PRIVATE_DATA
|
|
103
|
+
const attackerReadRes = await fetch('http://127.0.0.1:31000/proxy/attacker/private')
|
|
104
|
+
t.assert.strictEqual(attackerReadRes.status, 200)
|
|
105
|
+
const attackerReadBody = await attackerReadRes.json()
|
|
106
|
+
t.assert.strictEqual(
|
|
107
|
+
attackerReadBody.marker,
|
|
108
|
+
'ATTACKER_UPSTREAM',
|
|
109
|
+
'attacker request must reach attacker upstream, not victim'
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
// 2. attacker write — should write to attacker, not victim
|
|
113
|
+
const attackerWriteRes = await fetch('http://127.0.0.1:31000/proxy/attacker/write', {
|
|
114
|
+
method: 'POST',
|
|
115
|
+
headers: { 'content-type': 'application/json' },
|
|
116
|
+
body: JSON.stringify({ changedBy: 'attacker' })
|
|
117
|
+
})
|
|
118
|
+
t.assert.strictEqual(attackerWriteRes.status, 200)
|
|
119
|
+
|
|
120
|
+
// 3. verify victim state is unchanged (victim.written should be undefined/null)
|
|
121
|
+
const victimStateRes = await fetch('http://127.0.0.1:31000/proxy/victim/state')
|
|
122
|
+
t.assert.strictEqual(victimStateRes.status, 200)
|
|
123
|
+
const victimStateBody = await victimStateRes.json()
|
|
124
|
+
t.assert.strictEqual(
|
|
125
|
+
victimStateBody.written,
|
|
126
|
+
undefined,
|
|
127
|
+
'victim state must not be affected by attacker write'
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
t.test('GHSA-v574-6498-x57v: cache key collision via ambiguous concatenation', async (t) => {
|
|
132
|
+
t.plan(6)
|
|
133
|
+
await run(t)
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
t.test('GHSA-v574-6498-x57v: cache key delimiter cannot be injected', async (t) => {
|
|
137
|
+
const victim = Fastify()
|
|
138
|
+
victim.get('/*', async (req) => ({ marker: 'VICTIM', path: req.url }))
|
|
139
|
+
t.after(() => victim.close())
|
|
140
|
+
const victimOrigin = await victim.listen({ host: '127.0.0.1', port: 0 })
|
|
141
|
+
|
|
142
|
+
const attacker = Fastify()
|
|
143
|
+
attacker.get('/*', async (req) => ({ marker: 'ATTACKER', path: req.url }))
|
|
144
|
+
t.after(() => attacker.close())
|
|
145
|
+
const attackerOrigin = await attacker.listen({ host: '127.0.0.1', port: 0 })
|
|
146
|
+
|
|
147
|
+
const victimPair = {
|
|
148
|
+
source: 'private',
|
|
149
|
+
dest: `${victimOrigin}/|${attackerOrigin}/`
|
|
150
|
+
}
|
|
151
|
+
const attackerPair = {
|
|
152
|
+
source: `private|${victimOrigin}/`,
|
|
153
|
+
dest: `${attackerOrigin}/`
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// These pairs collide when the key is source + '|' + dest.
|
|
157
|
+
t.assert.strictEqual(
|
|
158
|
+
victimPair.source + '|' + victimPair.dest,
|
|
159
|
+
attackerPair.source + '|' + attackerPair.dest
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
const proxy = Fastify()
|
|
163
|
+
proxy.register(From, {
|
|
164
|
+
base: 'http://localhost',
|
|
165
|
+
http: true
|
|
166
|
+
})
|
|
167
|
+
proxy.get('/prime', (req, reply) => {
|
|
168
|
+
reply.from(victimPair.source, {
|
|
169
|
+
getUpstream: () => victimPair.dest
|
|
170
|
+
})
|
|
171
|
+
})
|
|
172
|
+
proxy.get('/collide', (req, reply) => {
|
|
173
|
+
reply.from(attackerPair.source, {
|
|
174
|
+
getUpstream: () => attackerPair.dest
|
|
175
|
+
})
|
|
176
|
+
})
|
|
177
|
+
t.after(() => proxy.close())
|
|
178
|
+
|
|
179
|
+
const primeResponse = await proxy.inject('/prime')
|
|
180
|
+
t.assert.strictEqual(primeResponse.statusCode, 200)
|
|
181
|
+
t.assert.strictEqual(primeResponse.json().marker, 'VICTIM')
|
|
182
|
+
|
|
183
|
+
const attackerResponse = await proxy.inject('/collide')
|
|
184
|
+
t.assert.strictEqual(attackerResponse.statusCode, 200)
|
|
185
|
+
t.assert.strictEqual(
|
|
186
|
+
attackerResponse.json().marker,
|
|
187
|
+
'ATTACKER',
|
|
188
|
+
'attacker request must not reuse the victim cache entry'
|
|
189
|
+
)
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
t.test('GHSA-v574-6498-x57v: cache key distinguishes an absent base', async (t) => {
|
|
193
|
+
const target = Fastify()
|
|
194
|
+
target.get('/private', async () => ({ marker: 'TARGET' }))
|
|
195
|
+
t.after(() => target.close())
|
|
196
|
+
const targetOrigin = await target.listen({ host: '127.0.0.1', port: 0 })
|
|
197
|
+
const source = `${targetOrigin}/private`
|
|
198
|
+
|
|
199
|
+
const proxy = Fastify()
|
|
200
|
+
proxy.register(From, { http: true })
|
|
201
|
+
proxy.get('/prime', (req, reply) => reply.from(source))
|
|
202
|
+
proxy.get('/invalid', (req, reply) => {
|
|
203
|
+
reply.from(source, {
|
|
204
|
+
getUpstream: () => 'undefined'
|
|
205
|
+
})
|
|
206
|
+
})
|
|
207
|
+
t.after(() => proxy.close())
|
|
208
|
+
|
|
209
|
+
const primeResponse = await proxy.inject('/prime')
|
|
210
|
+
t.assert.strictEqual(primeResponse.statusCode, 200)
|
|
211
|
+
t.assert.strictEqual(primeResponse.json().marker, 'TARGET')
|
|
212
|
+
|
|
213
|
+
const invalidResponse = await proxy.inject('/invalid')
|
|
214
|
+
t.assert.strictEqual(
|
|
215
|
+
invalidResponse.statusCode,
|
|
216
|
+
500,
|
|
217
|
+
'invalid upstream must not reuse the entry cached without a base'
|
|
218
|
+
)
|
|
219
|
+
})
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const t = require('node:test')
|
|
4
|
+
const Fastify = require('fastify')
|
|
5
|
+
const From = require('..')
|
|
6
|
+
const http = require('node:http')
|
|
7
|
+
const { once } = require('node:events')
|
|
8
|
+
|
|
9
|
+
// See https://github.com/fastify/fastify-reply-from/issues/419
|
|
10
|
+
// When a client aborts an HTTP/1 request before the upstream target responds,
|
|
11
|
+
// reply-from must not attempt to forward the (late) upstream response: it should
|
|
12
|
+
// neither invoke the onResponse callback nor pipe the upstream body to the
|
|
13
|
+
// already-closed reply. The equivalent HTTP/2 path was already handled; this
|
|
14
|
+
// covers the HTTP/1 path.
|
|
15
|
+
t.test('does not forward the upstream response when the HTTP/1 request was aborted', async (t) => {
|
|
16
|
+
t.plan(1)
|
|
17
|
+
|
|
18
|
+
// Target that delays its response long enough for the client to abort first.
|
|
19
|
+
const target = http.createServer((_req, res) => {
|
|
20
|
+
setTimeout(() => {
|
|
21
|
+
res.statusCode = 200
|
|
22
|
+
res.end('hello world')
|
|
23
|
+
}, 500)
|
|
24
|
+
})
|
|
25
|
+
t.after(() => target.close())
|
|
26
|
+
target.listen({ port: 0 })
|
|
27
|
+
await once(target, 'listening')
|
|
28
|
+
|
|
29
|
+
const instance = Fastify()
|
|
30
|
+
t.after(() => instance.close())
|
|
31
|
+
|
|
32
|
+
let onResponseCalled = false
|
|
33
|
+
instance.register(From)
|
|
34
|
+
instance.get('/', (_request, reply) => {
|
|
35
|
+
reply.from(`http://localhost:${target.address().port}`, {
|
|
36
|
+
onResponse (_req, replyInner, res) {
|
|
37
|
+
onResponseCalled = true
|
|
38
|
+
replyInner.send(res.stream)
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
instance.listen({ port: 0 })
|
|
44
|
+
await once(instance.server, 'listening')
|
|
45
|
+
|
|
46
|
+
await new Promise((resolve) => {
|
|
47
|
+
const req = http.request({
|
|
48
|
+
host: 'localhost',
|
|
49
|
+
port: instance.server.address().port,
|
|
50
|
+
path: '/'
|
|
51
|
+
}, (res) => { res.resume() })
|
|
52
|
+
req.on('error', () => {})
|
|
53
|
+
req.end()
|
|
54
|
+
// Abort well before the target responds.
|
|
55
|
+
setTimeout(() => req.destroy(), 50)
|
|
56
|
+
// Wait past the target's response so any (buggy) forwarding would have happened.
|
|
57
|
+
setTimeout(resolve, 800)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
t.assert.strictEqual(onResponseCalled, false, 'onResponse must not run for an aborted request')
|
|
61
|
+
})
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { once } = require('node:events')
|
|
4
|
+
const http = require('node:http')
|
|
5
|
+
const net = require('node:net')
|
|
6
|
+
const t = require('node:test')
|
|
7
|
+
const Fastify = require('fastify')
|
|
8
|
+
const From = require('..')
|
|
9
|
+
|
|
10
|
+
for (const [name, options] of [
|
|
11
|
+
['undici', {}],
|
|
12
|
+
['node:http', { undici: false }]
|
|
13
|
+
]) {
|
|
14
|
+
t.test(`closes the downstream connection when ${name} responds before the request is complete`, async (t) => {
|
|
15
|
+
const target = http.createServer((_req, res) => {
|
|
16
|
+
res.end('early')
|
|
17
|
+
})
|
|
18
|
+
const instance = Fastify()
|
|
19
|
+
|
|
20
|
+
t.after(async () => {
|
|
21
|
+
instance.server.closeAllConnections()
|
|
22
|
+
target.closeAllConnections()
|
|
23
|
+
await instance.close()
|
|
24
|
+
if (target.listening) await new Promise(resolve => target.close(resolve))
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
await new Promise(resolve => target.listen({ port: 0 }, resolve))
|
|
28
|
+
|
|
29
|
+
instance.register(From, options)
|
|
30
|
+
instance.addContentTypeParser('application/octet-stream', function (_request, payload, done) {
|
|
31
|
+
done(null, payload)
|
|
32
|
+
})
|
|
33
|
+
instance.post('/', (_request, reply) => {
|
|
34
|
+
reply.from(`http://localhost:${target.address().port}`)
|
|
35
|
+
})
|
|
36
|
+
await instance.listen({ port: 0 })
|
|
37
|
+
|
|
38
|
+
const socket = net.connect(instance.server.address().port, '127.0.0.1')
|
|
39
|
+
t.after(() => socket.destroy())
|
|
40
|
+
await once(socket, 'connect')
|
|
41
|
+
|
|
42
|
+
const responsePromise = new Promise((resolve, reject) => {
|
|
43
|
+
let response = ''
|
|
44
|
+
const timeout = setTimeout(() => {
|
|
45
|
+
reject(new Error('downstream connection was not closed'))
|
|
46
|
+
}, 5000)
|
|
47
|
+
|
|
48
|
+
socket.on('data', chunk => {
|
|
49
|
+
response += chunk.toString('latin1')
|
|
50
|
+
})
|
|
51
|
+
socket.once('error', err => {
|
|
52
|
+
clearTimeout(timeout)
|
|
53
|
+
reject(err)
|
|
54
|
+
})
|
|
55
|
+
socket.once('close', () => {
|
|
56
|
+
clearTimeout(timeout)
|
|
57
|
+
resolve(response)
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
socket.write([
|
|
62
|
+
'POST / HTTP/1.1',
|
|
63
|
+
'Host: localhost',
|
|
64
|
+
'Content-Type: application/octet-stream',
|
|
65
|
+
'Content-Length: 1048576',
|
|
66
|
+
'Connection: keep-alive',
|
|
67
|
+
'',
|
|
68
|
+
'a'
|
|
69
|
+
].join('\r\n'))
|
|
70
|
+
|
|
71
|
+
const response = await responsePromise
|
|
72
|
+
t.assert.match(response, /^HTTP\/1\.1 200 OK\r\n/)
|
|
73
|
+
t.assert.match(response, /\r\nconnection: close\r\n/i)
|
|
74
|
+
t.assert.match(response, /\r\n\r\nearly$/)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
t.test(`keeps the downstream connection reusable after ${name} consumes the request`, async (t) => {
|
|
78
|
+
const target = http.createServer((req, res) => {
|
|
79
|
+
req.resume()
|
|
80
|
+
req.on('end', () => res.end('complete'))
|
|
81
|
+
})
|
|
82
|
+
const agent = new http.Agent({ keepAlive: true, maxSockets: 1 })
|
|
83
|
+
const instance = Fastify()
|
|
84
|
+
|
|
85
|
+
t.after(async () => {
|
|
86
|
+
agent.destroy()
|
|
87
|
+
instance.server.closeAllConnections()
|
|
88
|
+
target.closeAllConnections()
|
|
89
|
+
await instance.close()
|
|
90
|
+
if (target.listening) await new Promise(resolve => target.close(resolve))
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
await new Promise(resolve => target.listen({ port: 0 }, resolve))
|
|
94
|
+
|
|
95
|
+
instance.register(From, options)
|
|
96
|
+
instance.addContentTypeParser('application/octet-stream', function (_request, payload, done) {
|
|
97
|
+
done(null, payload)
|
|
98
|
+
})
|
|
99
|
+
instance.post('/', (_request, reply) => {
|
|
100
|
+
reply.from(`http://localhost:${target.address().port}`)
|
|
101
|
+
})
|
|
102
|
+
await instance.listen({ port: 0 })
|
|
103
|
+
|
|
104
|
+
const first = await makeRequest(instance.server.address().port, agent, 'first')
|
|
105
|
+
t.assert.notStrictEqual(first.headers.connection, 'close')
|
|
106
|
+
|
|
107
|
+
const second = await makeRequest(instance.server.address().port, agent, 'second')
|
|
108
|
+
t.assert.strictEqual(second.reusedSocket, true)
|
|
109
|
+
t.assert.strictEqual(second.body, 'complete')
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function makeRequest (port, agent, body) {
|
|
114
|
+
return new Promise((resolve, reject) => {
|
|
115
|
+
const req = http.request({
|
|
116
|
+
agent,
|
|
117
|
+
method: 'POST',
|
|
118
|
+
hostname: '127.0.0.1',
|
|
119
|
+
port,
|
|
120
|
+
path: '/',
|
|
121
|
+
headers: {
|
|
122
|
+
'content-length': Buffer.byteLength(body),
|
|
123
|
+
'content-type': 'application/octet-stream'
|
|
124
|
+
}
|
|
125
|
+
}, res => {
|
|
126
|
+
let responseBody = ''
|
|
127
|
+
res.setEncoding('utf8')
|
|
128
|
+
res.on('data', chunk => {
|
|
129
|
+
responseBody += chunk
|
|
130
|
+
})
|
|
131
|
+
res.on('end', () => {
|
|
132
|
+
resolve({
|
|
133
|
+
body: responseBody,
|
|
134
|
+
headers: res.headers,
|
|
135
|
+
reusedSocket: req.reusedSocket
|
|
136
|
+
})
|
|
137
|
+
})
|
|
138
|
+
})
|
|
139
|
+
req.on('error', reject)
|
|
140
|
+
req.end(body)
|
|
141
|
+
})
|
|
142
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { expect } from 'tstyche'
|
|
2
|
+
import fastify, {
|
|
3
|
+
type FastifyReply,
|
|
4
|
+
type FastifyRequest,
|
|
5
|
+
type RawServerBase,
|
|
6
|
+
type RequestGenericInterface,
|
|
7
|
+
type RouteGenericInterface
|
|
8
|
+
} from 'fastify'
|
|
9
|
+
import * as http from 'node:http'
|
|
10
|
+
import { type IncomingHttpHeaders } from 'node:http2'
|
|
11
|
+
import * as https from 'node:https'
|
|
12
|
+
import { Agent, Client, Dispatcher, Pool } from 'undici'
|
|
13
|
+
import replyFrom, {
|
|
14
|
+
type FastifyReplyFromOptions,
|
|
15
|
+
type RawServerResponse
|
|
16
|
+
} from '.'
|
|
17
|
+
|
|
18
|
+
const fullOptions: FastifyReplyFromOptions = {
|
|
19
|
+
base: 'http://example2.com',
|
|
20
|
+
http: {
|
|
21
|
+
agentOptions: {
|
|
22
|
+
keepAliveMsecs: 60 * 1000,
|
|
23
|
+
maxFreeSockets: 2048,
|
|
24
|
+
maxSockets: 2048
|
|
25
|
+
},
|
|
26
|
+
requestOptions: { timeout: 1000 },
|
|
27
|
+
agents: {
|
|
28
|
+
'http:': new http.Agent({}),
|
|
29
|
+
'https:': new https.Agent({})
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
http2: {
|
|
33
|
+
sessionTimeout: 1000,
|
|
34
|
+
requestTimeout: 1000,
|
|
35
|
+
sessionOptions: { rejectUnauthorized: true },
|
|
36
|
+
requestOptions: { endStream: true }
|
|
37
|
+
},
|
|
38
|
+
cacheURLs: 100,
|
|
39
|
+
disableCache: false,
|
|
40
|
+
undici: {
|
|
41
|
+
connections: 100,
|
|
42
|
+
pipelining: 10,
|
|
43
|
+
proxy: 'http://example2.com:8080'
|
|
44
|
+
},
|
|
45
|
+
contentTypesToEncode: ['application/x-www-form-urlencoded'],
|
|
46
|
+
retryMethods: ['GET', 'HEAD', 'OPTIONS', 'TRACE'],
|
|
47
|
+
maxRetriesOn503: 10,
|
|
48
|
+
disableRequestLogging: false,
|
|
49
|
+
globalAgent: false,
|
|
50
|
+
destroyAgent: true
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const app = fastify()
|
|
54
|
+
|
|
55
|
+
app.register(replyFrom)
|
|
56
|
+
app.register(replyFrom, {})
|
|
57
|
+
app.register(replyFrom, { http2: true })
|
|
58
|
+
app.register(replyFrom, fullOptions)
|
|
59
|
+
app.register(replyFrom, { undici: { proxy: new URL('http://example2.com:8080') } })
|
|
60
|
+
app.register(replyFrom, { undici: { proxy: { uri: 'http://example2.com:8080' } } })
|
|
61
|
+
|
|
62
|
+
app.register(replyFrom, { base: 'http://example.com', undici: new Agent() })
|
|
63
|
+
app.register(replyFrom, { base: 'http://example.com', undici: new Pool('http://example.com') })
|
|
64
|
+
app.register(replyFrom, { base: 'http://example.com', undici: new Client('http://example.com') })
|
|
65
|
+
app.register(replyFrom, { base: 'http://example.com', undici: new Dispatcher() })
|
|
66
|
+
|
|
67
|
+
app.get('/v1', (_request, reply) => {
|
|
68
|
+
expect(reply.from()).type.toBe<FastifyReply>()
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
app.get('/v3', (_request, reply) => {
|
|
72
|
+
reply.from('/v3', {
|
|
73
|
+
timeout: 1000,
|
|
74
|
+
body: { hello: 'world' },
|
|
75
|
+
rewriteRequestHeaders (req, headers) {
|
|
76
|
+
expect(req).type.toBe<FastifyRequest<RequestGenericInterface, RawServerBase>>()
|
|
77
|
+
return headers
|
|
78
|
+
},
|
|
79
|
+
getUpstream (req, base) {
|
|
80
|
+
expect(req).type.toBe<FastifyRequest<RequestGenericInterface, RawServerBase>>()
|
|
81
|
+
return base
|
|
82
|
+
},
|
|
83
|
+
onResponse (request, reply, res) {
|
|
84
|
+
expect(request).type.toBe<FastifyRequest<RequestGenericInterface, RawServerBase>>()
|
|
85
|
+
expect(reply).type.toBe<FastifyReply<RouteGenericInterface, RawServerBase>>()
|
|
86
|
+
expect(res).type.toBe<RawServerResponse<RawServerBase>>()
|
|
87
|
+
expect(res.statusCode).type.toBe<number>()
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
app.get('/http2', (_request, reply) => {
|
|
93
|
+
reply.from('/', {
|
|
94
|
+
method: 'POST',
|
|
95
|
+
retryDelay: ({ req, res, err, attempt, getDefaultDelay }) => {
|
|
96
|
+
const defaultDelay = getDefaultDelay(req, res, err, attempt)
|
|
97
|
+
expect(defaultDelay).type.toBe<number | null>()
|
|
98
|
+
|
|
99
|
+
if (res && res.statusCode === 500 && req.method === 'GET') {
|
|
100
|
+
return 300
|
|
101
|
+
}
|
|
102
|
+
return null
|
|
103
|
+
},
|
|
104
|
+
rewriteHeaders (headers) {
|
|
105
|
+
return headers
|
|
106
|
+
},
|
|
107
|
+
rewriteRequestHeaders (_req, headers: IncomingHttpHeaders) {
|
|
108
|
+
return headers
|
|
109
|
+
},
|
|
110
|
+
onError (reply: FastifyReply<RouteGenericInterface, RawServerBase>, error) {
|
|
111
|
+
return reply.send(error.error)
|
|
112
|
+
},
|
|
113
|
+
queryString (search, reqUrl, request) {
|
|
114
|
+
expect(search).type.toBe<string | undefined>()
|
|
115
|
+
expect(reqUrl).type.toBe<string>()
|
|
116
|
+
expect(request).type.toBe<FastifyRequest<RequestGenericInterface, RawServerBase>>()
|
|
117
|
+
return ''
|
|
118
|
+
},
|
|
119
|
+
})
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
expect<FastifyReplyFromOptions>().type.toBeAssignableFrom(fullOptions)
|
|
123
|
+
expect<FastifyReplyFromOptions>().type.toBeAssignableFrom({ http2: true })
|
package/types/index.test-d.ts
DELETED
|
@@ -1,194 +0,0 @@
|
|
|
1
|
-
import fastify, { FastifyReply, FastifyRequest, RawServerBase, RequestGenericInterface, RouteGenericInterface } from 'fastify'
|
|
2
|
-
import * as http from 'node:http'
|
|
3
|
-
import { IncomingHttpHeaders } from 'node:http2'
|
|
4
|
-
import * as https from 'node:https'
|
|
5
|
-
import { AddressInfo } from 'node:net'
|
|
6
|
-
import { expectType } from 'tsd'
|
|
7
|
-
import { Agent, Client, Dispatcher, Pool } from 'undici'
|
|
8
|
-
import replyFrom, { FastifyReplyFromOptions, RawServerResponse } from '..'
|
|
9
|
-
// @ts-ignore
|
|
10
|
-
import tap from 'tap'
|
|
11
|
-
|
|
12
|
-
const fullOptions: FastifyReplyFromOptions = {
|
|
13
|
-
base: 'http://example2.com',
|
|
14
|
-
http: {
|
|
15
|
-
agentOptions: {
|
|
16
|
-
keepAliveMsecs: 60 * 1000,
|
|
17
|
-
maxFreeSockets: 2048,
|
|
18
|
-
maxSockets: 2048
|
|
19
|
-
},
|
|
20
|
-
requestOptions: {
|
|
21
|
-
timeout: 1000
|
|
22
|
-
},
|
|
23
|
-
agents: {
|
|
24
|
-
'http:': new http.Agent({}),
|
|
25
|
-
'https:': new https.Agent({})
|
|
26
|
-
}
|
|
27
|
-
},
|
|
28
|
-
http2: {
|
|
29
|
-
sessionTimeout: 1000,
|
|
30
|
-
requestTimeout: 1000,
|
|
31
|
-
sessionOptions: {
|
|
32
|
-
rejectUnauthorized: true
|
|
33
|
-
},
|
|
34
|
-
requestOptions: {
|
|
35
|
-
endStream: true
|
|
36
|
-
}
|
|
37
|
-
},
|
|
38
|
-
cacheURLs: 100,
|
|
39
|
-
disableCache: false,
|
|
40
|
-
undici: {
|
|
41
|
-
connections: 100,
|
|
42
|
-
pipelining: 10,
|
|
43
|
-
proxy: 'http://example2.com:8080'
|
|
44
|
-
},
|
|
45
|
-
contentTypesToEncode: ['application/x-www-form-urlencoded'],
|
|
46
|
-
retryMethods: ['GET', 'HEAD', 'OPTIONS', 'TRACE'],
|
|
47
|
-
maxRetriesOn503: 10,
|
|
48
|
-
disableRequestLogging: false,
|
|
49
|
-
globalAgent: false,
|
|
50
|
-
destroyAgent: true
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
async function main () {
|
|
54
|
-
const server = fastify()
|
|
55
|
-
|
|
56
|
-
server.register(replyFrom)
|
|
57
|
-
|
|
58
|
-
server.register(replyFrom, {})
|
|
59
|
-
|
|
60
|
-
server.register(replyFrom, { http2: true })
|
|
61
|
-
|
|
62
|
-
server.register(replyFrom, fullOptions)
|
|
63
|
-
|
|
64
|
-
server.register(replyFrom, { undici: { proxy: new URL('http://example2.com:8080') } })
|
|
65
|
-
|
|
66
|
-
server.register(replyFrom, { undici: { proxy: { uri: 'http://example2.com:8080' } } })
|
|
67
|
-
|
|
68
|
-
server.get('/v1', (_request, reply) => {
|
|
69
|
-
expectType<FastifyReply>(reply.from())
|
|
70
|
-
})
|
|
71
|
-
server.get('/v3', (_request, reply) => {
|
|
72
|
-
reply.from('/v3', {
|
|
73
|
-
timeout: 1000,
|
|
74
|
-
body: { hello: 'world' },
|
|
75
|
-
rewriteRequestHeaders (req, headers) {
|
|
76
|
-
expectType<FastifyRequest<RequestGenericInterface, RawServerBase>>(req)
|
|
77
|
-
return headers
|
|
78
|
-
},
|
|
79
|
-
getUpstream (req, base) {
|
|
80
|
-
expectType<FastifyRequest<RequestGenericInterface, RawServerBase>>(req)
|
|
81
|
-
return base
|
|
82
|
-
},
|
|
83
|
-
onResponse (request, reply, res) {
|
|
84
|
-
expectType<FastifyRequest<RequestGenericInterface, RawServerBase>>(request)
|
|
85
|
-
expectType<FastifyReply<RouteGenericInterface, RawServerBase>>(reply)
|
|
86
|
-
expectType<RawServerResponse<RawServerBase>>(res)
|
|
87
|
-
expectType<number>(res.statusCode)
|
|
88
|
-
}
|
|
89
|
-
})
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
// http2
|
|
93
|
-
const instance = fastify({ http2: true })
|
|
94
|
-
// @ts-ignore
|
|
95
|
-
tap.tearDown(instance.close.bind(instance))
|
|
96
|
-
const target = fastify({ http2: true })
|
|
97
|
-
// @ts-ignore
|
|
98
|
-
tap.tearDown(target.close.bind(target))
|
|
99
|
-
instance.get('/', (_request, reply) => {
|
|
100
|
-
reply.from()
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
instance.get('/http2', (_request, reply) => {
|
|
104
|
-
reply.from('/', {
|
|
105
|
-
method: 'POST',
|
|
106
|
-
retryDelay: ({ req, res, err, attempt, getDefaultDelay }) => {
|
|
107
|
-
const defaultDelay = getDefaultDelay(req, res, err, attempt)
|
|
108
|
-
if (defaultDelay) return defaultDelay
|
|
109
|
-
|
|
110
|
-
if (res && res.statusCode === 500 && req.method === 'GET') {
|
|
111
|
-
return 300
|
|
112
|
-
}
|
|
113
|
-
return null
|
|
114
|
-
},
|
|
115
|
-
rewriteHeaders (headers) {
|
|
116
|
-
return headers
|
|
117
|
-
},
|
|
118
|
-
rewriteRequestHeaders (_req, headers: IncomingHttpHeaders) {
|
|
119
|
-
return headers
|
|
120
|
-
},
|
|
121
|
-
getUpstream (_req, base) {
|
|
122
|
-
return base
|
|
123
|
-
},
|
|
124
|
-
onError (reply: FastifyReply<RouteGenericInterface, RawServerBase>, error) {
|
|
125
|
-
return reply.send(error.error)
|
|
126
|
-
},
|
|
127
|
-
queryString (search, reqUrl, request) {
|
|
128
|
-
expectType<string | undefined>(search)
|
|
129
|
-
expectType<string>(reqUrl)
|
|
130
|
-
expectType<FastifyRequest<RequestGenericInterface, RawServerBase>>(request)
|
|
131
|
-
return ''
|
|
132
|
-
},
|
|
133
|
-
})
|
|
134
|
-
})
|
|
135
|
-
|
|
136
|
-
await target.listen({ port: 0 })
|
|
137
|
-
const port = (target.server.address() as AddressInfo).port
|
|
138
|
-
instance.register(replyFrom, {
|
|
139
|
-
base: `http://localhost:${port}`,
|
|
140
|
-
http2: {
|
|
141
|
-
sessionOptions: {
|
|
142
|
-
rejectUnauthorized: false,
|
|
143
|
-
},
|
|
144
|
-
},
|
|
145
|
-
})
|
|
146
|
-
instance.register(replyFrom, {
|
|
147
|
-
base: `http://localhost:${port}`,
|
|
148
|
-
http2: true,
|
|
149
|
-
})
|
|
150
|
-
await instance.listen({ port: 0 })
|
|
151
|
-
|
|
152
|
-
const undiciInstance = fastify()
|
|
153
|
-
undiciInstance.register(replyFrom, {
|
|
154
|
-
base: 'http://example2.com',
|
|
155
|
-
undici: {
|
|
156
|
-
pipelining: 10,
|
|
157
|
-
connections: 10
|
|
158
|
-
}
|
|
159
|
-
})
|
|
160
|
-
await undiciInstance.ready()
|
|
161
|
-
|
|
162
|
-
const undiciInstanceAgent = fastify()
|
|
163
|
-
undiciInstance.register(replyFrom, {
|
|
164
|
-
base: 'http://example2.com',
|
|
165
|
-
undici: new Agent()
|
|
166
|
-
})
|
|
167
|
-
await undiciInstanceAgent.ready()
|
|
168
|
-
|
|
169
|
-
const undiciInstancePool = fastify()
|
|
170
|
-
undiciInstance.register(replyFrom, {
|
|
171
|
-
base: 'http://example2.com',
|
|
172
|
-
undici: new Pool('http://example2.com')
|
|
173
|
-
})
|
|
174
|
-
await undiciInstancePool.ready()
|
|
175
|
-
|
|
176
|
-
const undiciInstanceClient = fastify()
|
|
177
|
-
undiciInstance.register(replyFrom, {
|
|
178
|
-
base: 'http://example2.com',
|
|
179
|
-
undici: new Client('http://example2.com')
|
|
180
|
-
})
|
|
181
|
-
await undiciInstanceClient.ready()
|
|
182
|
-
|
|
183
|
-
const undiciInstanceDispatcher = fastify()
|
|
184
|
-
undiciInstance.register(replyFrom, {
|
|
185
|
-
base: 'http://example2.com',
|
|
186
|
-
undici: new Dispatcher()
|
|
187
|
-
})
|
|
188
|
-
await undiciInstanceDispatcher.ready()
|
|
189
|
-
|
|
190
|
-
tap.pass('done')
|
|
191
|
-
tap.end()
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
main()
|