@fastify/reply-from 12.6.1 → 12.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.
- package/.github/dependabot.yml +42 -2
- package/.github/workflows/ci.yml +1 -1
- package/.github/workflows/lock-threads.yml +19 -0
- package/README.md +3 -3
- package/index.js +19 -1
- package/lib/utils.js +6 -1
- package/package.json +8 -9
- package/test/build-url.test.js +9 -0
- package/test/incomplete-request.test.js +142 -0
- package/test/strip-connection-headers.test.js +76 -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/.github/workflows/ci.yml
CHANGED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
name: Lock Threads
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
schedule:
|
|
5
|
+
- cron: '0 0 1 * *'
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
|
|
8
|
+
concurrency:
|
|
9
|
+
group: lock
|
|
10
|
+
|
|
11
|
+
permissions:
|
|
12
|
+
contents: read
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
lock-threads:
|
|
16
|
+
permissions:
|
|
17
|
+
issues: write
|
|
18
|
+
pull-requests: write
|
|
19
|
+
uses: fastify/workflows/.github/workflows/lock-threads.yml@v6
|
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
|
@@ -11,6 +11,7 @@ const {
|
|
|
11
11
|
filterPseudoHeaders,
|
|
12
12
|
copyHeaders,
|
|
13
13
|
stripHttp1ConnectionHeaders,
|
|
14
|
+
getConnectionHeaders,
|
|
14
15
|
buildURL
|
|
15
16
|
} = require('./lib/utils')
|
|
16
17
|
|
|
@@ -89,6 +90,18 @@ const fastifyReplyFrom = fp(function from (fastify, opts, next) {
|
|
|
89
90
|
|
|
90
91
|
const sourceHttp2 = req.httpVersionMajor === 2
|
|
91
92
|
const headers = sourceHttp2 ? filterPseudoHeaders(req.headers) : { ...req.headers }
|
|
93
|
+
|
|
94
|
+
// Strip client-supplied Connection header and all header names listed in it
|
|
95
|
+
// before rewriteRequestHeaders runs. This prevents clients from stripping
|
|
96
|
+
// headers added by the proxy itself.
|
|
97
|
+
const connectionHeaderNames = getConnectionHeaders(headers)
|
|
98
|
+
if (headers.connection || connectionHeaderNames.length > 0) {
|
|
99
|
+
delete headers.connection
|
|
100
|
+
for (let i = 0; i < connectionHeaderNames.length; i++) {
|
|
101
|
+
delete headers[connectionHeaderNames[i]]
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
92
105
|
headers.host = url.host
|
|
93
106
|
const qs = getQueryString(url.search, req.url, opts, this.request)
|
|
94
107
|
let body = ''
|
|
@@ -133,7 +146,7 @@ const fastifyReplyFrom = fp(function from (fastify, opts, next) {
|
|
|
133
146
|
}
|
|
134
147
|
}
|
|
135
148
|
|
|
136
|
-
// according to https://
|
|
149
|
+
// according to https://datatracker.ietf.org/doc/html/rfc2616#section-4.3
|
|
137
150
|
// fastify ignore message body when it's a GET or HEAD request
|
|
138
151
|
// when proxy this request, we should reset the content-length to make it a valid http request
|
|
139
152
|
// discussion: https://github.com/fastify/fastify/issues/953
|
|
@@ -205,6 +218,11 @@ const fastifyReplyFrom = fp(function from (fastify, opts, next) {
|
|
|
205
218
|
)
|
|
206
219
|
} else {
|
|
207
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
|
+
}
|
|
208
226
|
}
|
|
209
227
|
try {
|
|
210
228
|
this.code(res.statusCode)
|
package/lib/utils.js
CHANGED
|
@@ -92,7 +92,12 @@ function stripHttp1ConnectionHeaders (headers) {
|
|
|
92
92
|
|
|
93
93
|
// issue ref: https://github.com/fastify/fast-proxy/issues/42
|
|
94
94
|
function buildURL (source, reqBase) {
|
|
95
|
-
|
|
95
|
+
// Check for path traversal: '..' must be a complete path segment to be an
|
|
96
|
+
// attack. A bare substring match incorrectly rejects legitimate URLs that
|
|
97
|
+
// contain '...' (e.g. Next.js catch-all routes like '[...slug]').
|
|
98
|
+
// See: https://github.com/fastify/fastify-reply-from/issues/460
|
|
99
|
+
const decoded = decodeURIComponent(source)
|
|
100
|
+
if (decoded === '..' || decoded.includes('/..') || decoded.includes('../')) {
|
|
96
101
|
const err = new Error('source/request contain invalid characters')
|
|
97
102
|
err.statusCode = 400
|
|
98
103
|
throw err
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fastify/reply-from",
|
|
3
|
-
"version": "12.6.
|
|
3
|
+
"version": "12.6.3",
|
|
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,28 +59,27 @@
|
|
|
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",
|
|
69
68
|
"form-data": "^4.0.0",
|
|
70
69
|
"h2url": "^0.2.0",
|
|
71
|
-
"neostandard": "^0.
|
|
70
|
+
"neostandard": "^0.13.0",
|
|
72
71
|
"nock": "^14.0.0",
|
|
73
|
-
"proxy": "^
|
|
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
|
},
|
package/test/build-url.test.js
CHANGED
|
@@ -77,6 +77,15 @@ test('should throw when trying to override base', async (t) => {
|
|
|
77
77
|
await Promise.all(promises)
|
|
78
78
|
})
|
|
79
79
|
|
|
80
|
+
test('should not throw on URLs containing "..." (e.g. Next.js catch-all routes)', (t) => {
|
|
81
|
+
t.plan(2)
|
|
82
|
+
// Next.js catch-all routes like [...slug] appear percent-encoded in static chunk URLs.
|
|
83
|
+
// These contain '...' as a substring but are not path traversal.
|
|
84
|
+
// See: https://github.com/fastify/fastify-reply-from/issues/460
|
|
85
|
+
t.assert.doesNotThrow(() => buildURL('/_next/static/chunks/pages/%5B...slug%5D.js', 'http://localhost'))
|
|
86
|
+
t.assert.doesNotThrow(() => buildURL('/_next/static/chunks/pages/%5B%5B...slug%5D%5D.js', 'http://localhost'))
|
|
87
|
+
})
|
|
88
|
+
|
|
80
89
|
test('should throw on path traversal attempts', (t) => {
|
|
81
90
|
t.assert.throws(
|
|
82
91
|
() => buildURL('/foo/bar/../', 'http://localhost'),
|
|
@@ -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
|
+
}
|
|
@@ -230,3 +230,79 @@ t.test('handles Connection header with keep-alive and custom headers (undici)',
|
|
|
230
230
|
t.assert.strictEqual(result.statusCode, 200)
|
|
231
231
|
t.assert.strictEqual(result.body, 'ok')
|
|
232
232
|
})
|
|
233
|
+
|
|
234
|
+
t.test('does not strip headers added by rewriteRequestHeaders (undici)', async (t) => {
|
|
235
|
+
t.plan(4)
|
|
236
|
+
const instance = Fastify()
|
|
237
|
+
instance.register(From)
|
|
238
|
+
|
|
239
|
+
t.after(() => instance.close())
|
|
240
|
+
|
|
241
|
+
let seenForwardedBy
|
|
242
|
+
const target = http.createServer((req, res) => {
|
|
243
|
+
seenForwardedBy = req.headers['x-forwarded-by']
|
|
244
|
+
t.assert.ok('request proxied')
|
|
245
|
+
res.statusCode = 200
|
|
246
|
+
res.setHeader('Content-Type', 'text/plain')
|
|
247
|
+
res.end('ok')
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
instance.get('/', (_request, reply) => {
|
|
251
|
+
reply.from(`http://localhost:${target.address().port}`, {
|
|
252
|
+
rewriteRequestHeaders: (_request, headers) => {
|
|
253
|
+
return { ...headers, 'x-forwarded-by': 'fastify-proxy' }
|
|
254
|
+
}
|
|
255
|
+
})
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
t.after(() => target.close())
|
|
259
|
+
|
|
260
|
+
await new Promise((resolve) => instance.listen({ port: 0 }, resolve))
|
|
261
|
+
await new Promise((resolve) => target.listen({ port: 0 }, resolve))
|
|
262
|
+
|
|
263
|
+
const result = await makeRequest(instance.server.address().port, {
|
|
264
|
+
Connection: 'X-Forwarded-By'
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
t.assert.strictEqual(result.statusCode, 200)
|
|
268
|
+
t.assert.strictEqual(result.body, 'ok')
|
|
269
|
+
t.assert.strictEqual(seenForwardedBy, 'fastify-proxy', 'X-Forwarded-By should not be stripped')
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
t.test('does not strip headers added by rewriteRequestHeaders (http)', async (t) => {
|
|
273
|
+
t.plan(4)
|
|
274
|
+
const instance = Fastify()
|
|
275
|
+
instance.register(From, { undici: false })
|
|
276
|
+
|
|
277
|
+
t.after(() => instance.close())
|
|
278
|
+
|
|
279
|
+
let seenForwardedBy
|
|
280
|
+
const target = http.createServer((req, res) => {
|
|
281
|
+
seenForwardedBy = req.headers['x-forwarded-by']
|
|
282
|
+
t.assert.ok('request proxied')
|
|
283
|
+
res.statusCode = 200
|
|
284
|
+
res.setHeader('Content-Type', 'text/plain')
|
|
285
|
+
res.end('ok')
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
instance.get('/', (_request, reply) => {
|
|
289
|
+
reply.from(`http://localhost:${target.address().port}`, {
|
|
290
|
+
rewriteRequestHeaders: (_request, headers) => {
|
|
291
|
+
return { ...headers, 'x-forwarded-by': 'fastify-proxy' }
|
|
292
|
+
}
|
|
293
|
+
})
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
t.after(() => target.close())
|
|
297
|
+
|
|
298
|
+
await new Promise((resolve) => instance.listen({ port: 0 }, resolve))
|
|
299
|
+
await new Promise((resolve) => target.listen({ port: 0 }, resolve))
|
|
300
|
+
|
|
301
|
+
const result = await makeRequest(instance.server.address().port, {
|
|
302
|
+
Connection: 'X-Forwarded-By'
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
t.assert.strictEqual(result.statusCode, 200)
|
|
306
|
+
t.assert.strictEqual(result.body, 'ok')
|
|
307
|
+
t.assert.strictEqual(seenForwardedBy, 'fastify-proxy', 'X-Forwarded-By should not be stripped')
|
|
308
|
+
})
|
|
@@ -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()
|