@fastify/reply-from 10.0.0 → 11.0.0

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.
Files changed (95) hide show
  1. package/.gitattributes +2 -0
  2. package/.github/workflows/ci.yml +1 -1
  3. package/.taprc +1 -9
  4. package/README.md +114 -29
  5. package/index.js +55 -47
  6. package/lib/request.js +32 -12
  7. package/lib/utils.js +8 -0
  8. package/package.json +25 -22
  9. package/test/async-route-handler.test.js +1 -1
  10. package/test/base-get.test.js +1 -1
  11. package/test/base-querystring.test.js +1 -1
  12. package/test/build-url.test.js +9 -2
  13. package/test/core-with-path-in-base.test.js +1 -1
  14. package/test/custom-undici-instance.test.js +1 -1
  15. package/test/disable-request-logging.test.js +1 -1
  16. package/test/fastify-multipart-incompatibility.test.js +3 -3
  17. package/test/fix-GHSA-v2v2-hph8-q5xp.test.js +47 -0
  18. package/test/full-delete-http2.test.js +52 -0
  19. package/test/full-get-test.test.js +1 -1
  20. package/test/full-https-get.test.js +3 -3
  21. package/test/full-post-extended-content-type.test.js +1 -1
  22. package/test/full-post-http2.test.js +2 -0
  23. package/test/full-post-stream-core.test.js +1 -1
  24. package/test/full-post-stream.test.js +1 -1
  25. package/test/full-post.test.js +1 -1
  26. package/test/full-querystring-rewrite-option-complex.test.js +1 -1
  27. package/test/full-querystring-rewrite-option-function-request.test.js +58 -0
  28. package/test/full-querystring-rewrite-option-function.test.js +2 -2
  29. package/test/full-querystring-rewrite-option.test.js +1 -1
  30. package/test/full-querystring-rewrite-string.test.js +1 -1
  31. package/test/full-querystring-rewrite.test.js +1 -1
  32. package/test/full-querystring.test.js +1 -1
  33. package/test/full-rewrite-body-content-type.test.js +1 -1
  34. package/test/full-rewrite-body-http.test.js +1 -1
  35. package/test/full-rewrite-body-to-empty-string.test.js +1 -1
  36. package/test/full-rewrite-body-to-null.test.js +1 -1
  37. package/test/full-rewrite-body.test.js +1 -1
  38. package/test/get-upstream-http.test.js +1 -1
  39. package/test/get-upstream-type.test.js +1 -1
  40. package/test/get-upstream-undici.test.js +1 -1
  41. package/test/get-with-body.test.js +1 -1
  42. package/test/head-with-body.test.js +7 -1
  43. package/test/http-agents.test.js +2 -2
  44. package/test/http-global-agent.test.js +1 -1
  45. package/test/http-http2.test.js +2 -0
  46. package/test/http-retry.test.js +1 -1
  47. package/test/http-timeout.test.js +0 -2
  48. package/test/http2-http2.test.js +4 -2
  49. package/test/http2-https.test.js +2 -2
  50. package/test/http2-invalid-base.test.js +1 -1
  51. package/test/http2-timeout-disabled.test.js +85 -0
  52. package/test/http2-timeout.test.js +2 -0
  53. package/test/http2-unix-socket.test.js +1 -1
  54. package/test/https-agents.test.js +4 -4
  55. package/test/https-global-agent.test.js +3 -3
  56. package/test/method.test.js +70 -0
  57. package/test/modifyCoreObjects-false.test.js +1 -1
  58. package/test/no-body-opts-with-get.test.js +1 -1
  59. package/test/no-body-opts-with-head.test.js +1 -1
  60. package/test/no-stream-body-option.test.js +2 -2
  61. package/test/on-error.test.js +11 -15
  62. package/test/onResponse.test.js +4 -3
  63. package/test/padded-body.test.js +1 -1
  64. package/test/post-formbody.test.js +1 -1
  65. package/test/post-plain-text.test.js +1 -1
  66. package/test/post-with-custom-encoded-contenttype.test.js +2 -2
  67. package/test/post-with-octet-stream.test.js +2 -2
  68. package/test/retry-on-503.test.js +1 -1
  69. package/test/retry-with-a-custom-handler.test.js +186 -0
  70. package/test/rewrite-headers-type.test.js +1 -1
  71. package/test/rewrite-headers.test.js +1 -1
  72. package/test/rewrite-request-headers-type.test.js +1 -1
  73. package/test/rewrite-request-headers.test.js +1 -1
  74. package/test/transform-body.test.js +3 -3
  75. package/test/undici-agent.test.js +2 -2
  76. package/test/undici-body.test.js +1 -1
  77. package/test/undici-connect-timeout.test.js +10 -14
  78. package/test/undici-custom-dispatcher.test.js +73 -0
  79. package/test/undici-global-agent.test.js +1 -1
  80. package/test/undici-no-destroy.test.js +29 -0
  81. package/test/undici-options.test.js +1 -1
  82. package/test/undici-proxy-agent.test.js +81 -0
  83. package/test/undici-retry.test.js +1 -1
  84. package/test/undici-timeout-body-partial.test.js +15 -19
  85. package/test/undici-timeout-body.test.js +13 -17
  86. package/test/undici-timeout.test.js +11 -15
  87. package/test/undici-with-path-in-base.test.js +1 -1
  88. package/test/undici.test.js +1 -1
  89. package/test/unix-http-undici-from.test.js +3 -3
  90. package/test/unix-http-undici.test.js +3 -3
  91. package/test/unix-http.test.js +3 -3
  92. package/test/unix-https-undici.test.js +4 -4
  93. package/test/unix-https.test.js +4 -4
  94. package/types/index.d.ts +37 -19
  95. package/types/index.test-d.ts +37 -10
package/.gitattributes ADDED
@@ -0,0 +1,2 @@
1
+ # Set default behavior to automatically convert line endings
2
+ * text=auto eol=lf
@@ -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@v5.0.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
@@ -14,7 +14,7 @@ npm i @fastify/reply-from
14
14
  ```
15
15
 
16
16
  ## Compatibility with @fastify/multipart
17
- `@fastify/reply-from` and [`@fastify/multipart`](https://github.com/fastify/fastify-multipart) should not be registered as sibling plugins nor should they be registered in plugins which have a parent-child relationship.<br> The two plugins are incompatible, in the sense that the behavior of `@fastify/reply-from` might not be the expected one when the above-mentioned conditions are not respected.<br> This is due to the fact that `@fastify/multipart` consumes the multipart content by parsing it, hence this content is not forwarded to the target service by `@fastify/reply-from`.<br>
17
+ `@fastify/reply-from` and [`@fastify/multipart`](https://github.com/fastify/fastify-multipart) should not be registered as sibling plugins nor should they be registered in plugins which have a parent-child relationship.`<br>` The two plugins are incompatible, in the sense that the behavior of `@fastify/reply-from` might not be the expected one when the above-mentioned conditions are not respected.`<br>` This is due to the fact that `@fastify/multipart` consumes the multipart content by parsing it, hence this content is not forwarded to the target service by `@fastify/reply-from`.`<br>`
18
18
  However, the two plugins may be used within the same fastify instance, at the condition that they belong to disjoint branches of the fastify plugins hierarchy tree.
19
19
 
20
20
  ## Usage
@@ -74,7 +74,7 @@ socket server by using `querystring.escape(socketPath)` as the hostname. This i
74
74
  for http2 nor undici. To illustrate:
75
75
 
76
76
  ```js
77
- const socketPath = require('querystring').escape('/run/http-daemon.socket')
77
+ const socketPath = require('node:querystring').escape('/run/http-daemon.socket')
78
78
  proxy.register(require('@fastify/reply-from'), {
79
79
  base: 'unix+http://${socketPath}/'
80
80
  });
@@ -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:
@@ -115,7 +127,7 @@ proxy.register(require('@fastify/reply-from'), {
115
127
 
116
128
  #### `http`
117
129
 
118
- Set the `http` option to an Object to use
130
+ Set the `http` option to an Object to use
119
131
  Node's [`http.request`](https://nodejs.org/api/http.html#http_http_request_options_callback)
120
132
  will be used if you do not enable [`http2`](#http2). To customize the `request`,
121
133
  you can pass in [`agentOptions`](https://nodejs.org/api/http.html#http_new_agent_options) and
@@ -136,6 +148,7 @@ proxy.register(require('@fastify/reply-from'), {
136
148
  ```
137
149
 
138
150
  You can also pass custom HTTP agents. If you pass the agents, then the http.agentOptions will be ignored. To illustrate:
151
+
139
152
  ```js
140
153
  proxy.register(require('@fastify/reply-from'), {
141
154
  base: 'http://localhost:3001/',
@@ -192,27 +205,27 @@ The number of parsed URLs that will be cached. Default: `100`.
192
205
 
193
206
  #### `disableCache`
194
207
 
195
- This option will disable the URL caching.
208
+ This option will disable the URL caching.
196
209
  This cache is dedicated to reduce the amount of URL object generation.
197
210
  Generating URLs is a main bottleneck of this module, please disable this cache with caution.
198
211
 
199
212
  #### `contentTypesToEncode`
200
213
 
201
- An array of content types whose response body will be passed through `JSON.stringify()`.
214
+ An array of content types whose response body will be passed through `JSON.stringify()`.
202
215
  This only applies when a custom [`body`](#body) is not passed in. Defaults to:
203
216
 
204
217
  ```js
205
- [
218
+ [
206
219
  'application/json'
207
220
  ]
208
221
  ```
209
222
 
210
223
  #### `retryMethods`
211
224
 
212
- On which methods should the connection be retried in case of socket hang up.
225
+ On which methods should the connection be retried in case of socket hang up.
213
226
  **Be aware** that setting here not idempotent method may lead to unexpected results on target.
214
227
 
215
- By default: `['GET', 'HEAD', 'OPTIONS', 'TRACE' ]`
228
+ By default: `['GET', 'HEAD', 'OPTIONS', 'TRACE']`
216
229
 
217
230
  This plugin will always retry on 503 errors, _unless_ `retryMethods` does not contain `GET`.
218
231
 
@@ -221,6 +234,7 @@ This plugin will always retry on 503 errors, _unless_ `retryMethods` does not co
221
234
  Enables the possibility to explictly opt-in for global agents.
222
235
 
223
236
  Usage for undici global agent:
237
+
224
238
  ```js
225
239
  import { setGlobalDispatcher, ProxyAgent } from 'undici'
226
240
 
@@ -234,11 +248,12 @@ fastify.register(FastifyReplyFrom, {
234
248
  ```
235
249
 
236
250
  Usage for http/https global agent:
251
+
237
252
  ```js
238
253
  fastify.register(FastifyReplyFrom, {
239
254
  base: 'http://localhost:3001/',
240
255
  // http and https is allowed to use http.globalAgent or https.globalAgent
241
- globalAgent: true,
256
+ globalAgent: true,
242
257
  http: {
243
258
  }
244
259
  })
@@ -246,6 +261,15 @@ fastify.register(FastifyReplyFrom, {
246
261
 
247
262
  ---
248
263
 
264
+ #### `destroyAgent`
265
+
266
+ If set to `true`, it will destroy all agents when the Fastify is closed.
267
+ If set to `false`, it will not destroy the agents.
268
+
269
+ By Default: `false`
270
+
271
+ ---
272
+
249
273
  #### `maxRetriesOn503`
250
274
 
251
275
  This plugin will always retry on `GET` requests that returns 503 errors, _unless_ `retryMethods` does not contain `GET`.
@@ -254,23 +278,77 @@ This option set the limit on how many times the plugin should retry the request,
254
278
 
255
279
  By Default: 10
256
280
 
281
+ ---
282
+
283
+ ### `retryDelay`
284
+
285
+ - `handler`. Required
286
+
287
+ This plugin gives the client an option to pass their own retry callback to allow the client to define what retryDelay they would like on any retries
288
+ outside the scope of what is handled by default in fastify-reply-from. To see the default please refer to index.js `getDefaultDelay()`
289
+ If a `handler` is passed to the `retryDelay` object the onus is on the client to invoke the default retry logic in their callback otherwise default cases such as 500 will not be handled
290
+
291
+ - `err` is the error thrown by making a request using whichever agent is configured
292
+ - `req` is the raw request details sent to the underlying agent. __Note__: this object is not a Fastify request object, but instead the low-level request for the agent.
293
+ - `res` is the raw response returned by the underlying agent (if available) __Note__: this object is not a Fastify response, but instead the low-level response from the agent. This property may be null if no response was obtained at all, like from a connection reset or timeout.
294
+ - `attempt` in the object callback refers to the current retriesAttempt number. You are given the freedom to use this in concert with the retryCount property set to handle retries
295
+ - `getDefaultRetry` refers to the default retry handler. If this callback returns not null and you wish to handle those case of errors simply invoke it as done below.
296
+ - `retriesCount` refers to the retriesCount property a client passes to reply-from. Note if the client does not explicitly set this value it will default to 0. The objective value here is to avoid hard-coding and seeing the retriesCount set. It is your perogative to ensure that you ensure the value here is as you wish (and not `0` if not intended to be as a result of a lack of not setting it).
297
+
298
+ Given example
299
+
300
+ ```js
301
+ const customRetryLogic = ({err, req, res, attempt, getDefaultRetry}) => {
302
+ //If this block is not included all non 500 errors will not be retried
303
+ const defaultDelay = getDefaultDelay();
304
+ if (defaultDelay) return defaultDelay();
305
+
306
+ //Custom retry logic
307
+ if (res && res.statusCode === 500 && req.method === 'GET') {
308
+ return 300
309
+ }
310
+
311
+ if (err && err.code == "UND_ERR_SOCKET"){
312
+ return 600
313
+ }
314
+
315
+ return null
316
+ }
317
+
318
+ .......
319
+
320
+ fastify.register(FastifyReplyFrom, {
321
+ base: 'http://localhost:3001/',
322
+ retryDelay: customRetryLogic
323
+ })
324
+
325
+ ```
326
+
327
+ Note the Typescript Equivalent
328
+ ```
329
+ const customRetryLogic = ({req, res, err, getDefaultRetry}: RetryDetails) => {
330
+ ...
331
+ }
332
+ ...
257
333
 
334
+ ```
258
335
  ---
259
336
 
260
337
  ### `reply.from(source, [opts])`
261
338
 
262
339
  The plugin decorates the
263
- [`Reply`](https://github.com/fastify/fastify/blob/master/docs/Reply.md)
340
+ [`Reply`](https://fastify.dev/docs/latest/Reference/Reply)
264
341
  instance with a `from` method, which will reply to the original request
265
342
  __from the desired source__. The options allows to override any part of
266
343
  the request or response being sent or received to/from the source.
267
344
 
268
345
  **Note: If `base` is specified in plugin options, the `source` here should not override the host/origin.**
269
346
 
270
- #### `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.
271
350
 
272
- Called when a HTTP response is received from the source.
273
- 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
274
352
  option is specified.
275
353
 
276
354
  When replying with a body of a different length it is necessary to remove
@@ -285,6 +363,8 @@ the `content-length` header.
285
363
  }
286
364
  ```
287
365
 
366
+ **Note**: `onResponse` is called after headers have already been sent. If you want to modify response headers, use the `rewriteHeaders` hook.
367
+
288
368
  #### `onError(reply, error)`
289
369
 
290
370
  Called when a HTTP response is received with error from the source.
@@ -319,6 +399,7 @@ ContraintStrategies.
319
399
  e.g.:
320
400
 
321
401
  Route grpc-web/http1 and grpc/http2 to different routes with a ContentType-ConstraintStrategy:
402
+
322
403
  ```
323
404
  const contentTypeMatchContraintStrategy = {
324
405
  // strategy name for referencing in the route handler `constraints` options
@@ -332,17 +413,18 @@ const contentTypeMatchContraintStrategy = {
332
413
  }
333
414
  },
334
415
  // function to get the value of the constraint from each incoming request
335
- deriveConstraint: (req: any, ctx: any) => {
416
+ deriveConstraint: (req: any, ctx: any) => {
336
417
  return req.headers['content-type']
337
418
  },
338
419
  // optional flag marking if handlers without constraints can match requests that have a value for this constraint
339
420
  mustMatchWhenDerived: true
340
421
  }
341
-
422
+
342
423
  server.addConstraintStrategy(contentTypeMatchContraintStrategy);
343
424
  ```
344
425
 
345
426
  and then 2 different upstreams with different register's:
427
+
346
428
  ```
347
429
  // grpc-web / http1
348
430
  server.register(fastifyHttpProxy, {
@@ -350,19 +432,18 @@ server.register(fastifyHttpProxy, {
350
432
  // therefore we have to transport to the grpc-web-proxy via http1
351
433
  http2: false,
352
434
  upstream: 'http://grpc-web-proxy',
353
- constraints: { "contentType": "application/grpc-web+proto" }
435
+ constraints: { "contentType": "application/grpc-web+proto" }
354
436
  });
355
437
 
356
438
  // grpc / http2
357
439
  server.register(fastifyHttpProxy, {
358
440
  http2: true,
359
441
  upstream: 'http://grpc.server',
360
- constraints: { "contentType": "application/grpc+proto" }
442
+ constraints: { "contentType": "application/grpc+proto" }
361
443
  });
362
444
  ```
363
445
 
364
-
365
- #### `queryString` or `queryString(search, reqUrl)`
446
+ #### `queryString` or `queryString(search, reqUrl, request)`
366
447
 
367
448
  Replaces the original querystring of the request with what is specified.
368
449
  This will be passed to
@@ -379,10 +460,14 @@ through `JSON.stringify()`.
379
460
  Setting this option for GET, HEAD requests will throw an error "Rewriting the body when doing a {GET|HEAD} is not allowed".
380
461
  Setting this option to `null` will strip the body (and `content-type` header) entirely from the proxied request.
381
462
 
463
+ #### `method`
464
+
465
+ Replaces the original request method with what is specified.
466
+
382
467
  #### `retriesCount`
383
468
 
384
- How many times it will try to pick another connection on socket hangup (`ECONNRESET` error).
385
- Useful when keeping the connection open (KeepAlive).
469
+ How many times it will try to pick another connection on socket hangup (`ECONNRESET` error).
470
+ Useful when keeping the connection open (KeepAlive).
386
471
  This number should be a function of the number of connections and the number of instances of a target.
387
472
 
388
473
  By default: 0 (disabled)
@@ -394,17 +479,17 @@ already overriding the [`body`](#body).
394
479
 
395
480
  ### Combining with [@fastify/formbody](https://github.com/fastify/fastify-formbody)
396
481
 
397
- `formbody` expects the body to be returned as a string and not an object.
482
+ `formbody` expects the body to be returned as a string and not an object.
398
483
  Use the [`contentTypesToEncode`](#contentTypesToEncode) option to pass in `['application/x-www-form-urlencoded']`
399
484
 
400
-
401
485
  ### HTTP & HTTP2 timeouts
402
486
 
403
487
  This library has:
488
+
404
489
  - `timeout` for `http` set by default. The default value is 10 seconds (`10000`).
405
490
  - `requestTimeout` & `sessionTimeout` for `http2` set by default.
406
- - The default value for `requestTimeout` is 10 seconds (`10000`).
407
- - 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.
408
493
 
409
494
  When a timeout happens, [`504 Gateway Timeout`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/504)
410
495
  will be returned to the client.
@@ -413,10 +498,10 @@ will be returned to the client.
413
498
 
414
499
  * [ ] support overriding the body with a stream
415
500
  * [ ] forward the request id to the other peer might require some
416
- refactoring because we have to make the `req.id` unique
417
- (see [hyperid](https://npm.im/hyperid)).
501
+ refactoring because we have to make the `req.id` unique
502
+ (see [hyperid](https://npm.im/hyperid)).
418
503
  * [ ] Support origin HTTP2 push
419
- * [x] benchmarks
504
+ * [X] benchmarks
420
505
 
421
506
  ## License
422
507
 
package/index.js CHANGED
@@ -1,9 +1,10 @@
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
- const Stream = require('stream')
6
+ const fastContentTypeParse = require('fast-content-type-parse')
7
+ const Stream = require('node:stream')
7
8
  const buildRequest = require('./lib/request')
8
9
  const {
9
10
  filterPseudoHeaders,
@@ -29,17 +30,18 @@ const fastifyReplyFrom = fp(function from (fastify, opts, next) {
29
30
  ])
30
31
 
31
32
  const retryMethods = new Set(opts.retryMethods || [
32
- 'GET', 'HEAD', 'OPTIONS', 'TRACE'
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
- globalAgent: opts.globalAgent
43
+ globalAgent: opts.globalAgent,
44
+ destroyAgent: opts.destroyAgent
43
45
  })
44
46
  if (requestBuilt instanceof Error) {
45
47
  next(requestBuilt)
@@ -51,6 +53,7 @@ const fastifyReplyFrom = fp(function from (fastify, opts, next) {
51
53
  fastify.decorateReply('from', function (source, opts) {
52
54
  opts = opts || {}
53
55
  const req = this.request.raw
56
+ const method = opts.method || req.method
54
57
  const onResponse = opts.onResponse
55
58
  const rewriteHeaders = opts.rewriteHeaders || headersNoOp
56
59
  const rewriteRequestHeaders = opts.rewriteRequestHeaders || requestHeadersNoOp
@@ -58,6 +61,7 @@ const fastifyReplyFrom = fp(function from (fastify, opts, next) {
58
61
  const onError = opts.onError || onErrorDefault
59
62
  const retriesCount = opts.retriesCount || 0
60
63
  const maxRetriesOn503 = opts.maxRetriesOn503 || 10
64
+ const retryDelay = opts.retryDelay || undefined
61
65
 
62
66
  if (!source) {
63
67
  source = req.url
@@ -77,7 +81,7 @@ const fastifyReplyFrom = fp(function from (fastify, opts, next) {
77
81
  const sourceHttp2 = req.httpVersionMajor === 2
78
82
  const headers = sourceHttp2 ? filterPseudoHeaders(req.headers) : { ...req.headers }
79
83
  headers.host = url.host
80
- const qs = getQueryString(url.search, req.url, opts)
84
+ const qs = getQueryString(url.search, req.url, opts, this.request)
81
85
  let body = ''
82
86
 
83
87
  if (opts.body !== undefined) {
@@ -105,15 +109,13 @@ const fastifyReplyFrom = fp(function from (fastify, opts, next) {
105
109
  body = this.request.body
106
110
  } else {
107
111
  // Per RFC 7231 §3.1.1.5 if this header is not present we MAY assume application/octet-stream
108
- const contentType = req.headers['content-type'] || 'application/octet-stream'
109
- // detect if body should be encoded as JSON
110
- // supporting extended content-type header formats:
111
- // - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type
112
- const lowerCaseContentType = contentType.toLowerCase()
113
- const plainContentType = lowerCaseContentType.indexOf(';') > -1
114
- ? lowerCaseContentType.slice(0, lowerCaseContentType.indexOf(';'))
115
- : lowerCaseContentType
116
- const shouldEncodeJSON = contentTypesToEncode.has(plainContentType)
112
+ let contentType = 'application/octet-stream'
113
+ if (req.headers['content-type']) {
114
+ const plainContentType = fastContentTypeParse.parse(req.headers['content-type'])
115
+ contentType = plainContentType.type
116
+ }
117
+
118
+ const shouldEncodeJSON = contentTypesToEncode.has(contentType)
117
119
  // transparently support JSON encoding
118
120
  body = shouldEncodeJSON ? JSON.stringify(this.request.body) : this.request.body
119
121
  // update origin request headers after encoding
@@ -126,12 +128,12 @@ const fastifyReplyFrom = fp(function from (fastify, opts, next) {
126
128
  // fastify ignore message body when it's a GET or HEAD request
127
129
  // when proxy this request, we should reset the content-length to make it a valid http request
128
130
  // discussion: https://github.com/fastify/fastify/issues/953
129
- if (req.method === 'GET' || req.method === 'HEAD') {
131
+ if (method === 'GET' || method === 'HEAD') {
130
132
  // body will be populated here only if opts.body is passed.
131
133
  // if we are doing that with a GET or HEAD request is a programmer error
132
134
  // and as such we can throw immediately.
133
135
  if (body) {
134
- throw new Error(`Rewriting the body when doing a ${req.method} is not allowed`)
136
+ throw new Error(`Rewriting the body when doing a ${method} is not allowed`)
135
137
  }
136
138
  }
137
139
 
@@ -140,13 +142,35 @@ const fastifyReplyFrom = fp(function from (fastify, opts, next) {
140
142
  const requestHeaders = rewriteRequestHeaders(this.request, headers)
141
143
  const contentLength = requestHeaders['content-length']
142
144
  let requestImpl
143
- if (retryMethods.has(req.method) && !contentLength) {
144
- requestImpl = createRequestRetry(request, this, retriesCount, retryOnError, maxRetriesOn503)
145
+
146
+ const getDefaultDelay = (req, res, err, retries) => {
147
+ if (retryMethods.has(method) && !contentLength) {
148
+ // Magic number, so why not 42? We might want to make this configurable.
149
+ let retryAfter = 42 * Math.random() * (retries + 1)
150
+
151
+ if (res && res.headers['retry-after']) {
152
+ retryAfter = res.headers['retry-after']
153
+ }
154
+ if (res && res.statusCode === 503 && req.method === 'GET') {
155
+ if (retriesCount === 0 && retries < maxRetriesOn503) {
156
+ return retryAfter
157
+ }
158
+ } else if (retriesCount > retries && err && err.code === retryOnError) {
159
+ return retryAfter
160
+ }
161
+ }
162
+ return null
163
+ }
164
+
165
+ if (retryDelay) {
166
+ requestImpl = createRequestRetry(request, this, (req, res, err, retries) => {
167
+ return retryDelay({ err, req, res, attempt: retries, getDefaultDelay, retriesCount })
168
+ })
145
169
  } else {
146
- requestImpl = request
170
+ requestImpl = createRequestRetry(request, this, getDefaultDelay)
147
171
  }
148
172
 
149
- requestImpl({ method: req.method, url, qs, headers: requestHeaders, body }, (err, res) => {
173
+ requestImpl({ method, url, qs, headers: requestHeaders, body }, (err, res) => {
150
174
  if (err) {
151
175
  this.request.log.warn(err, 'response errored')
152
176
  if (!this.sent) {
@@ -177,7 +201,7 @@ const fastifyReplyFrom = fp(function from (fastify, opts, next) {
177
201
  }
178
202
  this.code(res.statusCode)
179
203
  if (onResponse) {
180
- onResponse(this.request, this, res.stream)
204
+ onResponse(this.request, this, res)
181
205
  } else {
182
206
  this.send(res.stream)
183
207
  }
@@ -198,16 +222,15 @@ const fastifyReplyFrom = fp(function from (fastify, opts, next) {
198
222
  // actually destroy those sockets
199
223
  setImmediate(next)
200
224
  })
201
-
202
225
  next()
203
226
  }, {
204
- fastify: '4.x',
227
+ fastify: '5.x',
205
228
  name: '@fastify/reply-from'
206
229
  })
207
230
 
208
- function getQueryString (search, reqUrl, opts) {
231
+ function getQueryString (search, reqUrl, opts, request) {
209
232
  if (typeof opts.queryString === 'function') {
210
- return '?' + opts.queryString(search, reqUrl)
233
+ return '?' + opts.queryString(search, reqUrl, request)
211
234
  }
212
235
 
213
236
  if (opts.queryString) {
@@ -244,33 +267,18 @@ function onErrorDefault (reply, { error }) {
244
267
  }
245
268
 
246
269
  function isFastifyMultipartRegistered (fastify) {
247
- // TODO: remove fastify.hasContentTypeParser('multipart') in next major
248
- // It is used to be compatible with @fastify/multipart@<=7.3.0
249
- return (fastify.hasContentTypeParser('multipart') || fastify.hasContentTypeParser('multipart/form-data')) && fastify.hasRequestDecorator('multipart')
270
+ return fastify.hasContentTypeParser('multipart/form-data')
250
271
  }
251
272
 
252
- function createRequestRetry (requestImpl, reply, retriesCount, retryOnError, maxRetriesOn503) {
273
+ function createRequestRetry (requestImpl, reply, retryHandler) {
253
274
  function requestRetry (req, cb) {
254
275
  let retries = 0
255
276
 
256
277
  function run () {
257
278
  requestImpl(req, function (err, res) {
258
- // Magic number, so why not 42? We might want to make this configurable.
259
- let retryAfter = 42 * Math.random() * (retries + 1)
260
-
261
- if (res && res.headers['retry-after']) {
262
- retryAfter = res.headers['retry-after']
263
- }
264
- if (!reply.sent) {
265
- // always retry on 503 errors
266
- if (res && res.statusCode === 503 && req.method === 'GET') {
267
- if (retriesCount === 0 && retries < maxRetriesOn503) {
268
- // we should stop at some point
269
- return retry(retryAfter)
270
- }
271
- } else if (retriesCount > retries && err && err.code === retryOnError) {
272
- return retry(retryAfter)
273
- }
279
+ const retryDelay = retryHandler(req, res, err, retries)
280
+ if (!reply.sent && retryDelay) {
281
+ return retry(retryDelay)
274
282
  }
275
283
  cb(err, res)
276
284
  })
package/lib/request.js CHANGED
@@ -1,12 +1,12 @@
1
1
  'use strict'
2
- const http = require('http')
3
- const https = require('https')
4
- const querystring = require('querystring')
2
+ const http = require('node:http')
3
+ const https = require('node:https')
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
- const http2 = require('http2')
9
+ const http2 = require('node:http2')
10
10
 
11
11
  const {
12
12
  TimeoutError,
@@ -22,10 +22,17 @@ function shouldUseUndici (opts) {
22
22
  return true
23
23
  }
24
24
 
25
+ function isRequestable (obj) {
26
+ return obj !== null &&
27
+ typeof obj === 'object' &&
28
+ typeof obj.request === 'function'
29
+ }
30
+
25
31
  function isUndiciInstance (obj) {
26
32
  return obj instanceof undici.Pool ||
27
33
  obj instanceof undici.Client ||
28
- obj instanceof undici.Dispatcher
34
+ obj instanceof undici.Dispatcher ||
35
+ isRequestable(obj)
29
36
  }
30
37
 
31
38
  function buildRequest (opts) {
@@ -42,6 +49,7 @@ function buildRequest (opts) {
42
49
  const baseUrl = opts.base && new URL(opts.base).origin
43
50
  const undiciOpts = opts.undici || {}
44
51
  const globalAgent = opts.globalAgent
52
+ const destroyAgent = opts.destroyAgent
45
53
  let http2Client
46
54
  let undiciAgent
47
55
  let undiciInstance
@@ -75,7 +83,11 @@ function buildRequest (opts) {
75
83
  } else if (isUndiciInstance(opts.undici)) {
76
84
  undiciInstance = opts.undici
77
85
  } else if (!globalAgent) {
78
- 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
+ }
79
91
  } else {
80
92
  undiciAgent = undici.getGlobalDispatcher()
81
93
  }
@@ -85,7 +97,7 @@ function buildRequest (opts) {
85
97
  }
86
98
 
87
99
  function close () {
88
- if (globalAgent) {
100
+ if (globalAgent || destroyAgent === false) {
89
101
  return
90
102
  }
91
103
 
@@ -196,7 +208,8 @@ function buildRequest (opts) {
196
208
  ...stripHttp1ConnectionHeaders(opts.headers)
197
209
  }, http2Opts.requestOptions)
198
210
  const isGet = opts.method === 'GET' || opts.method === 'get'
199
- if (!isGet) {
211
+ const isDelete = opts.method === 'DELETE' || opts.method === 'delete'
212
+ if (!isGet && !isDelete) {
200
213
  end(req, opts.body, done)
201
214
  }
202
215
  req.setTimeout(http2Opts.requestTimeout, () => {
@@ -240,7 +253,7 @@ function end (req, body, cb) {
240
253
  if (!body || typeof body === 'string' || body instanceof Uint8Array) {
241
254
  req.end(body)
242
255
  } else if (body.pipe) {
243
- pump(body, req, err => {
256
+ pipeline(body, req, err => {
244
257
  if (err) cb(err)
245
258
  })
246
259
  } else {
@@ -259,10 +272,10 @@ function getHttp2Opts (opts) {
259
272
  }
260
273
  http2Opts.sessionOptions = http2Opts.sessionOptions || {}
261
274
 
262
- if (!http2Opts.sessionTimeout) {
275
+ if (http2Opts.sessionTimeout === undefined) {
263
276
  http2Opts.sessionTimeout = opts.sessionTimeout || 60000
264
277
  }
265
- if (!http2Opts.requestTimeout) {
278
+ if (http2Opts.requestTimeout === undefined) {
266
279
  http2Opts.requestTimeout = 10000
267
280
  }
268
281
  http2Opts.sessionOptions.rejectUnauthorized = http2Opts.sessionOptions.rejectUnauthorized || false
@@ -295,6 +308,13 @@ function getAgentOptions (opts) {
295
308
  }
296
309
  }
297
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
+
298
318
  function getUndiciOptions (opts = {}) {
299
319
  const res = {
300
320
  pipelining: 1,