@fastify/reply-from 9.7.0 → 10.0.0-pre.fv5.1

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.
@@ -17,7 +17,7 @@ on:
17
17
 
18
18
  jobs:
19
19
  test:
20
- uses: fastify/workflows/.github/workflows/plugins-ci.yml@v3
20
+ uses: fastify/workflows/.github/workflows/plugins-ci.yml@v4.1.0
21
21
  with:
22
22
  license-check: true
23
23
  lint: true
package/.taprc CHANGED
@@ -1,11 +1,3 @@
1
- ts: false
2
- jsx: false
3
- flow: false
4
-
5
- functions: 97
6
- lines: 96
7
- statements: 96
8
- branches: 96
9
-
1
+ disable-coverage: true
10
2
  files:
11
3
  - test/**/*.test.js
package/README.md CHANGED
@@ -82,7 +82,7 @@ proxy.register(require('@fastify/reply-from'), {
82
82
 
83
83
  #### `undici`
84
84
 
85
- By default, [undici](https://github.com/mcollina/undici) will be used to perform the HTTP/1.1
85
+ By default, [undici](https://github.com/nodejs/undici) will be used to perform the HTTP/1.1
86
86
  requests. Enabling this flag should guarantee
87
87
  20-50% more throughput.
88
88
 
@@ -102,6 +102,18 @@ proxy.register(require('@fastify/reply-from'), {
102
102
  }
103
103
  })
104
104
  ```
105
+
106
+ You can also include a proxy for the undici client:
107
+
108
+ ```js
109
+ proxy.register(require('@fastify/reply-from'), {
110
+ base: 'http://localhost:3001/',
111
+ undici: {
112
+ proxy: 'http://my.proxy.server:8080',
113
+ }
114
+ })
115
+ ```
116
+
105
117
  See undici own options for more configurations.
106
118
 
107
119
  You can also pass the plugin a custom instance:
@@ -325,17 +337,18 @@ const customRetryLogic = ({req, res, err, getDefaultRetry}: RetryDetails) => {
325
337
  ### `reply.from(source, [opts])`
326
338
 
327
339
  The plugin decorates the
328
- [`Reply`](https://github.com/fastify/fastify/blob/master/docs/Reply.md)
340
+ [`Reply`](https://fastify.dev/docs/latest/Reference/Reply)
329
341
  instance with a `from` method, which will reply to the original request
330
342
  __from the desired source__. The options allows to override any part of
331
343
  the request or response being sent or received to/from the source.
332
344
 
333
345
  **Note: If `base` is specified in plugin options, the `source` here should not override the host/origin.**
334
346
 
335
- #### `onResponse(request, reply, res)`
347
+ #### `onResponse(request, reply, response)`
348
+
349
+ Called when a HTTP response is received from the source. Passed the original source `request`, the in-progress reply to the source as `reply`, and the ongoing `response` from the upstream server.
336
350
 
337
- Called when a HTTP response is received from the source.
338
- The default behavior is `reply.send(res)`, which will be disabled if the
351
+ The default behavior is `reply.send(response.stream)`, which will be disabled if the
339
352
  option is specified.
340
353
 
341
354
  When replying with a body of a different length it is necessary to remove
@@ -350,6 +363,8 @@ the `content-length` header.
350
363
  }
351
364
  ```
352
365
 
366
+ **Note**: `onResponse` is called after headers have already been sent. If you want to modify response headers, use the `rewriteHeaders` hook.
367
+
353
368
  #### `onError(reply, error)`
354
369
 
355
370
  Called when a HTTP response is received with error from the source.
@@ -428,7 +443,7 @@ server.register(fastifyHttpProxy, {
428
443
  });
429
444
  ```
430
445
 
431
- #### `queryString` or `queryString(search, reqUrl)`
446
+ #### `queryString` or `queryString(search, reqUrl, request)`
432
447
 
433
448
  Replaces the original querystring of the request with what is specified.
434
449
  This will be passed to
@@ -473,8 +488,8 @@ This library has:
473
488
 
474
489
  - `timeout` for `http` set by default. The default value is 10 seconds (`10000`).
475
490
  - `requestTimeout` & `sessionTimeout` for `http2` set by default.
476
- - The default value for `requestTimeout` is 10 seconds (`10000`).
477
- - The default value for `sessionTimeout` is 60 seconds (`60000`).
491
+ - The default value for `requestTimeout` is 10 seconds (`10000`), a value of 0 disables the timeout.
492
+ - The default value for `sessionTimeout` is 60 seconds (`60000`), a value of 0 disables the timeout.
478
493
 
479
494
  When a timeout happens, [`504 Gateway Timeout`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/504)
480
495
  will be returned to the client.
package/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  const fp = require('fastify-plugin')
4
- const { lru } = require('tiny-lru')
4
+ const { LruMap } = require('toad-cache')
5
5
  const querystring = require('fast-querystring')
6
6
  const fastContentTypeParse = require('fast-content-type-parse')
7
7
  const Stream = require('node:stream')
@@ -32,12 +32,13 @@ const fastifyReplyFrom = fp(function from (fastify, opts, next) {
32
32
  const retryMethods = new Set(opts.retryMethods || [
33
33
  'GET', 'HEAD', 'OPTIONS', 'TRACE'])
34
34
 
35
- const cache = opts.disableCache ? undefined : lru(opts.cacheURLs || 100)
35
+ const cache = opts.disableCache ? undefined : new LruMap(opts.cacheURLs || 100)
36
36
  const base = opts.base
37
37
  const requestBuilt = buildRequest({
38
38
  http: opts.http,
39
39
  http2: opts.http2,
40
40
  base,
41
+ sessionTimeout: opts.sessionTimeout,
41
42
  undici: opts.undici,
42
43
  globalAgent: opts.globalAgent,
43
44
  destroyAgent: opts.destroyAgent
@@ -80,7 +81,7 @@ const fastifyReplyFrom = fp(function from (fastify, opts, next) {
80
81
  const sourceHttp2 = req.httpVersionMajor === 2
81
82
  const headers = sourceHttp2 ? filterPseudoHeaders(req.headers) : { ...req.headers }
82
83
  headers.host = url.host
83
- const qs = getQueryString(url.search, req.url, opts)
84
+ const qs = getQueryString(url.search, req.url, opts, this.request)
84
85
  let body = ''
85
86
 
86
87
  if (opts.body !== undefined) {
@@ -200,7 +201,7 @@ const fastifyReplyFrom = fp(function from (fastify, opts, next) {
200
201
  }
201
202
  this.code(res.statusCode)
202
203
  if (onResponse) {
203
- onResponse(this.request, this, res.stream)
204
+ onResponse(this.request, this, res)
204
205
  } else {
205
206
  this.send(res.stream)
206
207
  }
@@ -227,9 +228,9 @@ const fastifyReplyFrom = fp(function from (fastify, opts, next) {
227
228
  name: '@fastify/reply-from'
228
229
  })
229
230
 
230
- function getQueryString (search, reqUrl, opts) {
231
+ function getQueryString (search, reqUrl, opts, request) {
231
232
  if (typeof opts.queryString === 'function') {
232
- return '?' + opts.queryString(search, reqUrl)
233
+ return '?' + opts.queryString(search, reqUrl, request)
233
234
  }
234
235
 
235
236
  if (opts.queryString) {
@@ -266,9 +267,7 @@ function onErrorDefault (reply, { error }) {
266
267
  }
267
268
 
268
269
  function isFastifyMultipartRegistered (fastify) {
269
- // TODO: remove fastify.hasContentTypeParser('multipart') in next major
270
- // It is used to be compatible with @fastify/multipart@<=7.3.0
271
- return (fastify.hasContentTypeParser('multipart') || fastify.hasContentTypeParser('multipart/form-data')) && fastify.hasRequestDecorator('multipart')
270
+ return fastify.hasContentTypeParser('multipart/form-data')
272
271
  }
273
272
 
274
273
  function createRequestRetry (requestImpl, reply, retryHandler) {
package/lib/request.js CHANGED
@@ -3,7 +3,7 @@ const http = require('node:http')
3
3
  const https = require('node:https')
4
4
  const querystring = require('node:querystring')
5
5
  const eos = require('end-of-stream')
6
- const pump = require('pump')
6
+ const { pipeline } = require('node:stream')
7
7
  const undici = require('undici')
8
8
  const { stripHttp1ConnectionHeaders } = require('./utils')
9
9
  const http2 = require('node:http2')
@@ -83,7 +83,11 @@ function buildRequest (opts) {
83
83
  } else if (isUndiciInstance(opts.undici)) {
84
84
  undiciInstance = opts.undici
85
85
  } else if (!globalAgent) {
86
- undiciAgent = new undici.Agent(getUndiciOptions(opts.undici))
86
+ if (undiciOpts.proxy) {
87
+ undiciAgent = new undici.ProxyAgent(getUndiciProxyOptions(opts.undici))
88
+ } else {
89
+ undiciAgent = new undici.Agent(getUndiciOptions(opts.undici))
90
+ }
87
91
  } else {
88
92
  undiciAgent = undici.getGlobalDispatcher()
89
93
  }
@@ -204,7 +208,8 @@ function buildRequest (opts) {
204
208
  ...stripHttp1ConnectionHeaders(opts.headers)
205
209
  }, http2Opts.requestOptions)
206
210
  const isGet = opts.method === 'GET' || opts.method === 'get'
207
- if (!isGet) {
211
+ const isDelete = opts.method === 'DELETE' || opts.method === 'delete'
212
+ if (!isGet && !isDelete) {
208
213
  end(req, opts.body, done)
209
214
  }
210
215
  req.setTimeout(http2Opts.requestTimeout, () => {
@@ -248,7 +253,7 @@ function end (req, body, cb) {
248
253
  if (!body || typeof body === 'string' || body instanceof Uint8Array) {
249
254
  req.end(body)
250
255
  } else if (body.pipe) {
251
- pump(body, req, err => {
256
+ pipeline(body, req, err => {
252
257
  if (err) cb(err)
253
258
  })
254
259
  } else {
@@ -267,10 +272,10 @@ function getHttp2Opts (opts) {
267
272
  }
268
273
  http2Opts.sessionOptions = http2Opts.sessionOptions || {}
269
274
 
270
- if (!http2Opts.sessionTimeout) {
275
+ if (http2Opts.sessionTimeout === undefined) {
271
276
  http2Opts.sessionTimeout = opts.sessionTimeout || 60000
272
277
  }
273
- if (!http2Opts.requestTimeout) {
278
+ if (http2Opts.requestTimeout === undefined) {
274
279
  http2Opts.requestTimeout = 10000
275
280
  }
276
281
  http2Opts.sessionOptions.rejectUnauthorized = http2Opts.sessionOptions.rejectUnauthorized || false
@@ -303,6 +308,13 @@ function getAgentOptions (opts) {
303
308
  }
304
309
  }
305
310
 
311
+ function getUndiciProxyOptions ({ proxy, ...opts }) {
312
+ if (typeof proxy === 'string' || proxy instanceof URL) {
313
+ return getUndiciOptions({ uri: proxy, ...opts })
314
+ }
315
+ return getUndiciOptions({ ...proxy, ...opts })
316
+ }
317
+
306
318
  function getUndiciOptions (opts = {}) {
307
319
  const res = {
308
320
  pipelining: 1,
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@fastify/reply-from",
3
- "version": "9.7.0",
3
+ "version": "10.0.0-pre.fv5.1",
4
4
  "description": "forward your HTTP request to another server, for fastify",
5
5
  "main": "index.js",
6
6
  "type": "commonjs",
7
7
  "types": "types/index.d.ts",
8
8
  "scripts": {
9
9
  "lint": "standard | snazzy",
10
- "lint:fix": "standard --fix",
10
+ "lint:fix": "standard --fix | snazzy",
11
11
  "test": "npm run test:unit && npm run test:typescript",
12
12
  "test:unit": "tap",
13
13
  "test:typescript": "tsd"
@@ -30,35 +30,35 @@
30
30
  "homepage": "https://github.com/fastify/fastify-reply-from#readme",
31
31
  "devDependencies": {
32
32
  "@fastify/formbody": "^7.4.0",
33
- "@fastify/multipart": "^7.4.0",
34
- "@fastify/pre-commit": "^2.0.2",
35
- "@sinonjs/fake-timers": "^11.0.0",
36
- "@types/node": "^20.1.4",
37
- "@types/tap": "^15.0.7",
38
- "fastify": "^4.0.2",
33
+ "@fastify/multipart": "^8.2.0",
34
+ "@fastify/pre-commit": "^2.1.0",
35
+ "@sinonjs/fake-timers": "^11.2.2",
36
+ "@types/node": "^20.11.30",
37
+ "@types/tap": "^15.0.11",
38
+ "fastify": "^4.26.2",
39
39
  "form-data": "^4.0.0",
40
- "got": "^11.8.2",
40
+ "got": "^11.8.6",
41
41
  "h2url": "^0.2.0",
42
- "msgpack5": "^6.0.1",
43
- "nock": "^13.2.6",
42
+ "msgpack5": "^6.0.2",
43
+ "nock": "^13.5.4",
44
+ "proxy": "^2.1.1",
44
45
  "proxyquire": "^2.1.3",
45
- "semver": "^7.5.1",
46
+ "semver": "^7.6.0",
46
47
  "simple-get": "^4.0.1",
47
48
  "snazzy": "^9.0.0",
48
- "split2": "^4.1.0",
49
- "standard": "^17.0.0",
50
- "tap": "^16.2.0",
51
- "tsd": "^0.30.1"
49
+ "split2": "^4.2.0",
50
+ "standard": "^17.1.0",
51
+ "tap": "^18.7.2",
52
+ "tsd": "^0.31.0"
52
53
  },
53
54
  "dependencies": {
54
- "@fastify/error": "^3.0.0",
55
+ "@fastify/error": "^3.4.1",
55
56
  "end-of-stream": "^1.4.4",
56
57
  "fast-content-type-parse": "^1.1.0",
57
- "fast-querystring": "^1.0.0",
58
- "fastify-plugin": "^4.0.0",
59
- "pump": "^3.0.0",
60
- "tiny-lru": "^11.0.0",
61
- "undici": "^5.19.1"
58
+ "fast-querystring": "^1.1.2",
59
+ "fastify-plugin": "^4.5.1",
60
+ "toad-cache": "^3.7.0",
61
+ "undici": "^6.11.1"
62
62
  },
63
63
  "pre-commit": [
64
64
  "lint",
@@ -0,0 +1,52 @@
1
+ 'use strict'
2
+
3
+ const { test } = require('tap')
4
+ const Fastify = require('fastify')
5
+ const From = require('..')
6
+ const got = require('got')
7
+
8
+ test('http -> http2', async function (t) {
9
+ const instance = Fastify()
10
+
11
+ t.teardown(instance.close.bind(instance))
12
+
13
+ const target = Fastify({
14
+ http2: true
15
+ })
16
+
17
+ target.delete('/', (request, reply) => {
18
+ t.pass('request proxied')
19
+ reply.code(200).header('x-my-header', 'hello!').send({
20
+ hello: 'world'
21
+ })
22
+ })
23
+
24
+ instance.delete('/', (request, reply) => {
25
+ reply.from()
26
+ })
27
+
28
+ t.teardown(target.close.bind(target))
29
+
30
+ await target.listen({ port: 0 })
31
+
32
+ instance.register(From, {
33
+ base: `http://localhost:${target.server.address().port}`,
34
+ http2: true
35
+ })
36
+
37
+ await instance.listen({ port: 0 })
38
+
39
+ const { headers, body, statusCode } = await got(
40
+ `http://localhost:${instance.server.address().port}`,
41
+ {
42
+ method: 'DELETE',
43
+ responseType: 'json'
44
+ }
45
+ )
46
+ t.equal(statusCode, 200)
47
+ t.equal(headers['x-my-header'], 'hello!')
48
+ t.match(headers['content-type'], /application\/json/)
49
+ t.same(body, { hello: 'world' })
50
+ instance.close()
51
+ target.close()
52
+ })
@@ -46,4 +46,6 @@ test('http -> http2', async function (t) {
46
46
  t.equal(headers['x-my-header'], 'hello!')
47
47
  t.match(headers['content-type'], /application\/json/)
48
48
  t.same(body, { hello: 'world' })
49
+ instance.close()
50
+ target.close()
49
51
  })
@@ -0,0 +1,58 @@
1
+ 'use strict'
2
+
3
+ const t = require('tap')
4
+ const Fastify = require('fastify')
5
+ const From = require('..')
6
+ const http = require('node:http')
7
+ const get = require('simple-get').concat
8
+ const querystring = require('node:querystring')
9
+
10
+ const instance = Fastify()
11
+
12
+ instance.addHook('preHandler', (request, reply, done) => {
13
+ request.addedVal = 'test'
14
+ done()
15
+ })
16
+
17
+ t.plan(10)
18
+ t.teardown(instance.close.bind(instance))
19
+
20
+ const target = http.createServer((req, res) => {
21
+ t.pass('request proxied')
22
+ t.equal(req.method, 'GET')
23
+ t.equal(req.url, '/world?q=test')
24
+ res.statusCode = 205
25
+ res.setHeader('Content-Type', 'text/plain')
26
+ res.setHeader('x-my-header', 'hello!')
27
+ res.end('hello world')
28
+ })
29
+
30
+ instance.get('/hello', (request, reply) => {
31
+ reply.from(`http://localhost:${target.address().port}/world?a=b`, {
32
+ queryString (search, reqUrl, request) {
33
+ return querystring.stringify({ q: request.addedVal })
34
+ }
35
+ })
36
+ })
37
+
38
+ t.teardown(target.close.bind(target))
39
+
40
+ target.listen({ port: 0 }, (err) => {
41
+ t.error(err)
42
+
43
+ instance.register(From)
44
+
45
+ instance.listen({ port: 0 }, (err) => {
46
+ t.error(err)
47
+
48
+ get(`http://localhost:${instance.server.address().port}/hello?a=b`, (err, res, data) => {
49
+ t.error(err)
50
+ t.equal(res.headers['content-type'], 'text/plain')
51
+ t.equal(res.headers['x-my-header'], 'hello!')
52
+ t.equal(res.statusCode, 205)
53
+ t.equal(data.toString(), 'hello world')
54
+ instance.close()
55
+ target.close()
56
+ })
57
+ })
58
+ })
@@ -43,6 +43,8 @@ test('http -> http2', async (t) => {
43
43
  t.equal(err.response.headers['x-my-header'], 'hello!')
44
44
  t.match(err.response.headers['content-type'], /application\/json/)
45
45
  t.same(JSON.parse(err.response.body), { hello: 'world' })
46
+ instance.close()
47
+ target.close()
46
48
  return
47
49
  }
48
50
 
@@ -9,8 +9,6 @@ const FakeTimers = require('@sinonjs/fake-timers')
9
9
  const clock = FakeTimers.createClock()
10
10
 
11
11
  test('http request timeout', async (t) => {
12
- t.autoend(false)
13
-
14
12
  const target = Fastify()
15
13
  t.teardown(target.close.bind(target))
16
14
 
@@ -54,4 +54,6 @@ t.test('http2 -> http2', async (t) => {
54
54
  t.equal(headers['x-my-header'], 'hello!')
55
55
  t.match(headers['content-type'], /application\/json/)
56
56
  t.same(JSON.parse(body), { hello: 'world' })
57
+ instance.close()
58
+ target.close()
57
59
  })
@@ -7,7 +7,7 @@ const From = require('../index')
7
7
  test('http2 invalid base', async (t) => {
8
8
  const instance = Fastify()
9
9
 
10
- await t.rejects(instance.register(From, {
10
+ await t.rejects(async () => instance.register(From, {
11
11
  http2: { requestTimeout: 100 }
12
12
  }), new Error('Option base is required when http2 is true'))
13
13
  })
@@ -0,0 +1,85 @@
1
+ 'use strict'
2
+
3
+ const { test } = require('tap')
4
+ const Fastify = require('fastify')
5
+ const From = require('..')
6
+ const got = require('got')
7
+
8
+ test('http2 request timeout disabled', async (t) => {
9
+ const target = Fastify({ http2: true })
10
+ t.teardown(target.close.bind(target))
11
+
12
+ target.get('/', () => {
13
+ t.pass('request arrives')
14
+ })
15
+
16
+ await target.listen({ port: 0 })
17
+
18
+ const instance = Fastify()
19
+ t.teardown(instance.close.bind(instance))
20
+
21
+ instance.register(From, {
22
+ base: `http://localhost:${target.server.address().port}`,
23
+ http2: { requestTimeout: 0, sessionTimeout: 16000 }
24
+ })
25
+
26
+ instance.get('/', (request, reply) => {
27
+ reply.from(`http://localhost:${target.server.address().port}/`)
28
+ })
29
+
30
+ await instance.listen({ port: 0 })
31
+
32
+ const result = await Promise.race([
33
+ got.get(`http://localhost:${instance.server.address().port}/`, {
34
+ retry: 0
35
+ }),
36
+ new Promise(resolve => setTimeout(resolve, 11000, 'passed'))
37
+ ])
38
+
39
+ // if we wait 11000 ms without a timeout error, we assume disabling the timeout worked
40
+ // 10000 ms is the default timeout
41
+ t.equal(result, 'passed')
42
+ })
43
+
44
+ test('http2 session timeout disabled', async (t) => {
45
+ const target = Fastify({ http2: true })
46
+
47
+ target.get('/', () => {
48
+ t.pass('request arrives')
49
+ })
50
+
51
+ await target.listen({ port: 0 })
52
+
53
+ const instance = Fastify()
54
+
55
+ instance.register(From, {
56
+ sessionTimeout: 3000,
57
+ destroyAgent: true,
58
+ base: `http://localhost:${target.server.address().port}`,
59
+ http2: { requestTimeout: 0, sessionTimeout: 0 }
60
+ })
61
+
62
+ instance.get('/', (request, reply) => {
63
+ reply.from(`http://localhost:${target.server.address().port}/`)
64
+ })
65
+
66
+ await instance.listen({ port: 0 })
67
+
68
+ const request = got.get(`http://localhost:${instance.server.address().port}/`, {
69
+ retry: 0
70
+ })
71
+
72
+ const result = await Promise.race([
73
+ request,
74
+ new Promise(resolve => setTimeout(resolve, 4000, 'passed'))
75
+ ])
76
+
77
+ // clean up right after the timeout, otherwise test will hang
78
+ request.cancel()
79
+ target.close()
80
+ instance.close()
81
+
82
+ // if we wait 4000 ms without a timeout error, we assume disabling the session timeout for reply-from worked
83
+ // because we pass 3000 ms as session timeout to the Fastify options itself
84
+ t.equal(result, 'passed')
85
+ })
@@ -122,4 +122,6 @@ test('http2 sse removes request and session timeout test', async (t) => {
122
122
 
123
123
  const { statusCode } = await got.get(`http://localhost:${instance.server.address().port}/`, { retry: 0 })
124
124
  t.equal(statusCode, 200)
125
+ instance.close()
126
+ target.close()
125
127
  })
@@ -9,7 +9,7 @@ test('throw an error if http2 is used with a Unix socket destination', async t =
9
9
 
10
10
  const instance = Fastify()
11
11
 
12
- await t.rejects(instance.register(From, {
12
+ await t.rejects(async () => instance.register(From, {
13
13
  base: 'unix+http://localhost:1337',
14
14
  http2: { requestTimeout: 100 }
15
15
  }), new Error('Unix socket destination is not supported when http2 is true'))
@@ -8,21 +8,19 @@ const FakeTimers = require('@sinonjs/fake-timers')
8
8
 
9
9
  const clock = FakeTimers.createClock()
10
10
 
11
- t.autoend(false)
11
+ t.test('on-error', async (t) => {
12
+ const target = Fastify()
13
+ t.teardown(target.close.bind(target))
12
14
 
13
- const target = Fastify()
14
- t.teardown(target.close.bind(target))
15
+ target.get('/', (request, reply) => {
16
+ t.pass('request arrives')
15
17
 
16
- target.get('/', (request, reply) => {
17
- t.pass('request arrives')
18
-
19
- clock.setTimeout(() => {
20
- reply.status(200).send('hello world')
21
- t.end()
22
- }, 1000)
23
- })
18
+ clock.setTimeout(() => {
19
+ reply.status(200).send('hello world')
20
+ t.end()
21
+ }, 1000)
22
+ })
24
23
 
25
- async function main () {
26
24
  await target.listen({ port: 0 })
27
25
 
28
26
  const instance = Fastify()
@@ -63,6 +61,4 @@ async function main () {
63
61
  }
64
62
 
65
63
  t.fail()
66
- }
67
-
68
- main()
64
+ })
@@ -9,7 +9,7 @@ const get = require('simple-get').concat
9
9
  const instance = Fastify()
10
10
  instance.register(From)
11
11
 
12
- t.plan(8)
12
+ t.plan(9)
13
13
  t.teardown(instance.close.bind(instance))
14
14
 
15
15
  const target = http.createServer((req, res) => {
@@ -22,8 +22,9 @@ const target = http.createServer((req, res) => {
22
22
  instance.get('/', (request1, reply) => {
23
23
  reply.from(`http://localhost:${target.address().port}`, {
24
24
  onResponse: (request2, reply, res) => {
25
+ t.equal(res.statusCode, 200)
25
26
  t.equal(request1.raw, request2.raw)
26
- reply.send(res)
27
+ reply.send(res.stream)
27
28
  }
28
29
  })
29
30
  })
@@ -26,7 +26,7 @@ instance.get('/', (request, reply) => {
26
26
  reply.from(`http://localhost:${target.address().port}`, {
27
27
  onResponse: (request, reply, res) => {
28
28
  reply.send(
29
- res.pipe(
29
+ res.stream.pipe(
30
30
  new Transform({
31
31
  transform: function (chunk, enc, cb) {
32
32
  this.push(chunk.toString().toUpperCase())
@@ -31,7 +31,7 @@ target.listen({ port: 0 }, err => {
31
31
  const From = proxyquire('..', {
32
32
  './lib/request.js': proxyquire('../lib/request.js', {
33
33
  undici: proxyquire('undici', {
34
- './lib/agent': proxyquire('undici/lib/agent.js', {
34
+ './lib/dispatcher/agent': proxyquire('undici/lib/dispatcher/agent.js', {
35
35
  './pool': class Pool extends undici.Pool {
36
36
  constructor (url, options) {
37
37
  super(url, options)
@@ -7,18 +7,16 @@ const Fastify = require('fastify')
7
7
  const From = require('..')
8
8
  const got = require('got')
9
9
 
10
- t.autoend(false)
11
-
10
+ t.test('undici connect timeout', async (t) => {
12
11
  // never connect
13
- net.connect = function (options) {
14
- return new net.Socket(options)
15
- }
12
+ net.connect = function (options) {
13
+ return new net.Socket(options)
14
+ }
16
15
 
17
- const target = http.createServer((req, res) => {
18
- t.fail('target never called')
19
- })
16
+ const target = http.createServer((req, res) => {
17
+ t.fail('target never called')
18
+ })
20
19
 
21
- async function main () {
22
20
  t.plan(2)
23
21
  await target.listen({ port: 0 })
24
22
 
@@ -53,6 +51,4 @@ async function main () {
53
51
  }
54
52
 
55
53
  t.fail()
56
- }
57
-
58
- main()
54
+ })
@@ -0,0 +1,81 @@
1
+ 'use strict'
2
+
3
+ const { test } = require('tap')
4
+ const { createServer } = require('node:http')
5
+ const Fastify = require('fastify')
6
+ const get = require('simple-get').concat
7
+ const { createProxy } = require('proxy')
8
+ const From = require('..')
9
+
10
+ const configFormat = {
11
+ string: (value) => value,
12
+ 'url instance': (value) => new URL(value),
13
+ object: (value) => ({ uri: value })
14
+ }
15
+
16
+ for (const [description, format] of Object.entries(configFormat)) {
17
+ test(`use undici ProxyAgent to connect through proxy - configured via ${description}`, async (t) => {
18
+ t.plan(5)
19
+ const target = await buildServer()
20
+ const proxy = await buildProxy()
21
+ t.teardown(target.close.bind(target))
22
+ t.teardown(proxy.close.bind(proxy))
23
+
24
+ const targetUrl = `http://localhost:${target.address().port}`
25
+ const proxyUrl = `http://localhost:${proxy.address().port}`
26
+
27
+ proxy.on('connect', () => {
28
+ t.ok(true, 'should connect to proxy')
29
+ })
30
+
31
+ target.on('request', (req, res) => {
32
+ res.setHeader('content-type', 'application/json')
33
+ res.end(JSON.stringify({ hello: 'world' }))
34
+ })
35
+
36
+ const instance = Fastify()
37
+ t.teardown(instance.close.bind(instance))
38
+
39
+ instance.register(From, {
40
+ base: targetUrl,
41
+ undici: {
42
+ proxy: format(proxyUrl)
43
+ }
44
+ })
45
+
46
+ instance.get('/', (request, reply) => {
47
+ reply.from()
48
+ })
49
+
50
+ const executionFlow = () => new Promise((resolve) => {
51
+ instance.listen({ port: 0 }, err => {
52
+ t.error(err)
53
+
54
+ get(`http://localhost:${instance.server.address().port}`, (err, res, data) => {
55
+ t.error(err)
56
+ t.same(res.statusCode, 200)
57
+ t.match(JSON.parse(data.toString()), { hello: 'world' })
58
+ resolve()
59
+ instance.close()
60
+ target.close()
61
+ })
62
+ })
63
+ })
64
+
65
+ await executionFlow()
66
+ })
67
+ }
68
+
69
+ function buildServer () {
70
+ return new Promise((resolve) => {
71
+ const server = createServer()
72
+ server.listen(0, () => resolve(server))
73
+ })
74
+ }
75
+
76
+ function buildProxy () {
77
+ return new Promise((resolve) => {
78
+ const server = createProxy(createServer())
79
+ server.listen(0, () => resolve(server))
80
+ })
81
+ }
@@ -9,23 +9,21 @@ const FakeTimers = require('@sinonjs/fake-timers')
9
9
 
10
10
  const clock = FakeTimers.createClock()
11
11
 
12
- t.autoend(false)
13
-
14
- const target = http.createServer((req, res) => {
15
- t.pass('request proxied')
16
- req.on('data', () => undefined)
17
- req.on('end', () => {
18
- res.writeHead(200)
19
- res.flushHeaders()
20
- res.write('test')
21
- clock.setTimeout(() => {
22
- res.end()
23
- t.end()
24
- }, 1000)
12
+ t.test('undici body timeout', async (t) => {
13
+ const target = http.createServer((req, res) => {
14
+ t.pass('request proxied')
15
+ req.on('data', () => undefined)
16
+ req.on('end', () => {
17
+ res.writeHead(200)
18
+ res.flushHeaders()
19
+ res.write('test')
20
+ clock.setTimeout(() => {
21
+ res.end()
22
+ t.end()
23
+ }, 1000)
24
+ })
25
25
  })
26
- })
27
26
 
28
- async function main () {
29
27
  await target.listen({ port: 0 })
30
28
 
31
29
  const instance = Fastify()
@@ -55,6 +53,4 @@ async function main () {
55
53
  }
56
54
 
57
55
  t.fail()
58
- }
59
-
60
- main()
56
+ })
@@ -9,21 +9,19 @@ const FakeTimers = require('@sinonjs/fake-timers')
9
9
 
10
10
  const clock = FakeTimers.createClock()
11
11
 
12
- t.autoend(false)
13
-
14
- const target = http.createServer((req, res) => {
15
- t.pass('request proxied')
16
- req.on('data', () => undefined)
17
- req.on('end', () => {
18
- res.flushHeaders()
19
- clock.setTimeout(() => {
20
- res.end()
21
- t.end()
22
- }, 1000)
12
+ t.test('undici body timeout', async (t) => {
13
+ const target = http.createServer((req, res) => {
14
+ t.pass('request proxied')
15
+ req.on('data', () => undefined)
16
+ req.on('end', () => {
17
+ res.flushHeaders()
18
+ clock.setTimeout(() => {
19
+ res.end()
20
+ t.end()
21
+ }, 1000)
22
+ })
23
23
  })
24
- })
25
24
 
26
- async function main () {
27
25
  await target.listen({ port: 0 })
28
26
 
29
27
  const instance = Fastify()
@@ -58,6 +56,4 @@ async function main () {
58
56
  }
59
57
 
60
58
  t.fail()
61
- }
62
-
63
- main()
59
+ })
@@ -8,21 +8,19 @@ const FakeTimers = require('@sinonjs/fake-timers')
8
8
 
9
9
  const clock = FakeTimers.createClock()
10
10
 
11
- t.autoend(false)
11
+ t.test('undici request timeout', async (t) => {
12
+ const target = Fastify()
13
+ t.teardown(target.close.bind(target))
12
14
 
13
- const target = Fastify()
14
- t.teardown(target.close.bind(target))
15
+ target.get('/', (request, reply) => {
16
+ t.pass('request arrives')
15
17
 
16
- target.get('/', (request, reply) => {
17
- t.pass('request arrives')
18
-
19
- clock.setTimeout(() => {
20
- reply.status(200).send('hello world')
21
- t.end()
22
- }, 1000)
23
- })
18
+ clock.setTimeout(() => {
19
+ reply.status(200).send('hello world')
20
+ t.end()
21
+ }, 1000)
22
+ })
24
23
 
25
- async function main () {
26
24
  await target.listen({ port: 0 })
27
25
 
28
26
  const instance = Fastify()
@@ -57,6 +55,4 @@ async function main () {
57
55
  }
58
56
 
59
57
  t.fail()
60
- }
61
-
62
- main()
58
+ })
package/types/index.d.ts CHANGED
@@ -28,6 +28,7 @@ import {
28
28
  RequestOptions as SecureRequestOptions
29
29
  } from "https";
30
30
  import { Pool } from 'undici';
31
+ import { ProxyAgent } from 'undici';
31
32
 
32
33
  declare module "fastify" {
33
34
  interface FastifyReply {
@@ -40,13 +41,18 @@ declare module "fastify" {
40
41
 
41
42
  type FastifyReplyFrom = FastifyPluginCallback<fastifyReplyFrom.FastifyReplyFromOptions>
42
43
  declare namespace fastifyReplyFrom {
43
- type QueryStringFunction = (search: string | undefined, reqUrl: string) => string;
44
+ type QueryStringFunction = (
45
+ search: string | undefined,
46
+ reqUrl: string,
47
+ request: FastifyRequest<RequestGenericInterface, RawServerBase>
48
+ ) => string;
44
49
 
45
50
  export type RetryDetails = {
46
51
  err: Error;
47
52
  req: FastifyRequest<RequestGenericInterface, RawServerBase>;
48
53
  res: FastifyReply<RawServerBase>;
49
54
  attempt: number;
55
+ retriesCount: number;
50
56
  getDefaultDelay: () => number | null;
51
57
  }
52
58
  export interface FastifyReplyFromHooks {
@@ -98,7 +104,7 @@ declare namespace fastifyReplyFrom {
98
104
  disableCache?: boolean;
99
105
  http?: HttpOptions;
100
106
  http2?: Http2Options | boolean;
101
- undici?: Pool.Options;
107
+ undici?: Pool.Options & { proxy?: string | URL | ProxyAgent.Options };
102
108
  contentTypesToEncode?: string[];
103
109
  retryMethods?: (HTTPMethods | 'TRACE')[];
104
110
  maxRetriesOn503?: number;
@@ -1,4 +1,4 @@
1
- import fastify, { FastifyReply, FastifyRequest, RawServerBase, RequestGenericInterface } from "fastify";
1
+ import fastify, { FastifyReply, FastifyRequest, RawReplyDefaultExpression, RawServerBase, RequestGenericInterface } from "fastify";
2
2
  import * as http from 'http';
3
3
  import { IncomingHttpHeaders } from "http2";
4
4
  import * as https from 'https';
@@ -38,7 +38,8 @@ const fullOptions: FastifyReplyFromOptions = {
38
38
  disableCache: false,
39
39
  undici: {
40
40
  connections: 100,
41
- pipelining: 10
41
+ pipelining: 10,
42
+ proxy: 'http://example2.com:8080'
42
43
  },
43
44
  contentTypesToEncode: ['application/x-www-form-urlencoded'],
44
45
  retryMethods: ['GET', 'HEAD', 'OPTIONS', 'TRACE'],
@@ -47,7 +48,6 @@ const fullOptions: FastifyReplyFromOptions = {
47
48
  globalAgent: false,
48
49
  destroyAgent: true
49
50
  };
50
- tap.autoend(false);
51
51
 
52
52
  async function main() {
53
53
  const server = fastify();
@@ -60,6 +60,10 @@ async function main() {
60
60
 
61
61
  server.register(replyFrom, fullOptions);
62
62
 
63
+ server.register(replyFrom, { undici: { proxy: new URL('http://example2.com:8080') } });
64
+
65
+ server.register(replyFrom, { undici: { proxy: { uri: 'http://example2.com:8080' } } });
66
+
63
67
  server.get("/v1", (request, reply) => {
64
68
  expectType<FastifyReply>(reply.from());
65
69
  });
@@ -73,6 +77,12 @@ async function main() {
73
77
  getUpstream(req, base) {
74
78
  expectType<FastifyRequest<RequestGenericInterface, RawServerBase>>(req);
75
79
  return base;
80
+ },
81
+ onResponse(request, reply, res) {
82
+ expectType<FastifyRequest<RequestGenericInterface, RawServerBase>>(request);
83
+ expectType<FastifyReply<RawServerBase>>(reply);
84
+ expectType<RawReplyDefaultExpression<RawServerBase>>(res);
85
+ expectType<number>(res.statusCode);
76
86
  }
77
87
  });
78
88
  });
@@ -91,7 +101,7 @@ async function main() {
91
101
  instance.get("/http2", (request, reply) => {
92
102
  reply.from("/", {
93
103
  method: "POST",
94
- retryDelay: ({err, req, res, attempt, getDefaultDelay}) => {
104
+ retryDelay: ({err, req, res, attempt, retriesCount, getDefaultDelay }) => {
95
105
  const defaultDelay = getDefaultDelay();
96
106
  if (defaultDelay) return defaultDelay;
97
107
 
@@ -111,7 +121,13 @@ async function main() {
111
121
  },
112
122
  onError(reply: FastifyReply<RawServerBase>, error) {
113
123
  return reply.send(error.error);
114
- }
124
+ },
125
+ queryString(search, reqUrl, request) {
126
+ expectType<string | undefined>(search);
127
+ expectType<string>(reqUrl);
128
+ expectType<FastifyRequest<RequestGenericInterface, RawServerBase>>(request);
129
+ return '';
130
+ },
115
131
  });
116
132
  });
117
133