@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.
- package/.gitattributes +2 -0
- package/.github/workflows/ci.yml +1 -1
- package/.taprc +1 -9
- package/README.md +114 -29
- package/index.js +55 -47
- package/lib/request.js +32 -12
- package/lib/utils.js +8 -0
- package/package.json +25 -22
- package/test/async-route-handler.test.js +1 -1
- package/test/base-get.test.js +1 -1
- package/test/base-querystring.test.js +1 -1
- package/test/build-url.test.js +9 -2
- package/test/core-with-path-in-base.test.js +1 -1
- package/test/custom-undici-instance.test.js +1 -1
- package/test/disable-request-logging.test.js +1 -1
- package/test/fastify-multipart-incompatibility.test.js +3 -3
- package/test/fix-GHSA-v2v2-hph8-q5xp.test.js +47 -0
- package/test/full-delete-http2.test.js +52 -0
- package/test/full-get-test.test.js +1 -1
- package/test/full-https-get.test.js +3 -3
- package/test/full-post-extended-content-type.test.js +1 -1
- package/test/full-post-http2.test.js +2 -0
- package/test/full-post-stream-core.test.js +1 -1
- package/test/full-post-stream.test.js +1 -1
- package/test/full-post.test.js +1 -1
- package/test/full-querystring-rewrite-option-complex.test.js +1 -1
- package/test/full-querystring-rewrite-option-function-request.test.js +58 -0
- package/test/full-querystring-rewrite-option-function.test.js +2 -2
- package/test/full-querystring-rewrite-option.test.js +1 -1
- package/test/full-querystring-rewrite-string.test.js +1 -1
- package/test/full-querystring-rewrite.test.js +1 -1
- package/test/full-querystring.test.js +1 -1
- package/test/full-rewrite-body-content-type.test.js +1 -1
- package/test/full-rewrite-body-http.test.js +1 -1
- package/test/full-rewrite-body-to-empty-string.test.js +1 -1
- package/test/full-rewrite-body-to-null.test.js +1 -1
- package/test/full-rewrite-body.test.js +1 -1
- package/test/get-upstream-http.test.js +1 -1
- package/test/get-upstream-type.test.js +1 -1
- package/test/get-upstream-undici.test.js +1 -1
- package/test/get-with-body.test.js +1 -1
- package/test/head-with-body.test.js +7 -1
- package/test/http-agents.test.js +2 -2
- package/test/http-global-agent.test.js +1 -1
- package/test/http-http2.test.js +2 -0
- package/test/http-retry.test.js +1 -1
- package/test/http-timeout.test.js +0 -2
- package/test/http2-http2.test.js +4 -2
- package/test/http2-https.test.js +2 -2
- package/test/http2-invalid-base.test.js +1 -1
- package/test/http2-timeout-disabled.test.js +85 -0
- package/test/http2-timeout.test.js +2 -0
- package/test/http2-unix-socket.test.js +1 -1
- package/test/https-agents.test.js +4 -4
- package/test/https-global-agent.test.js +3 -3
- package/test/method.test.js +70 -0
- package/test/modifyCoreObjects-false.test.js +1 -1
- package/test/no-body-opts-with-get.test.js +1 -1
- package/test/no-body-opts-with-head.test.js +1 -1
- package/test/no-stream-body-option.test.js +2 -2
- package/test/on-error.test.js +11 -15
- package/test/onResponse.test.js +4 -3
- package/test/padded-body.test.js +1 -1
- package/test/post-formbody.test.js +1 -1
- package/test/post-plain-text.test.js +1 -1
- package/test/post-with-custom-encoded-contenttype.test.js +2 -2
- package/test/post-with-octet-stream.test.js +2 -2
- package/test/retry-on-503.test.js +1 -1
- package/test/retry-with-a-custom-handler.test.js +186 -0
- package/test/rewrite-headers-type.test.js +1 -1
- package/test/rewrite-headers.test.js +1 -1
- package/test/rewrite-request-headers-type.test.js +1 -1
- package/test/rewrite-request-headers.test.js +1 -1
- package/test/transform-body.test.js +3 -3
- package/test/undici-agent.test.js +2 -2
- package/test/undici-body.test.js +1 -1
- package/test/undici-connect-timeout.test.js +10 -14
- package/test/undici-custom-dispatcher.test.js +73 -0
- package/test/undici-global-agent.test.js +1 -1
- package/test/undici-no-destroy.test.js +29 -0
- package/test/undici-options.test.js +1 -1
- package/test/undici-proxy-agent.test.js +81 -0
- package/test/undici-retry.test.js +1 -1
- package/test/undici-timeout-body-partial.test.js +15 -19
- package/test/undici-timeout-body.test.js +13 -17
- package/test/undici-timeout.test.js +11 -15
- package/test/undici-with-path-in-base.test.js +1 -1
- package/test/undici.test.js +1 -1
- package/test/unix-http-undici-from.test.js +3 -3
- package/test/unix-http-undici.test.js +3 -3
- package/test/unix-http.test.js +3 -3
- package/test/unix-https-undici.test.js +4 -4
- package/test/unix-https.test.js +4 -4
- package/types/index.d.ts +37 -19
- package/types/index.test-d.ts +37 -10
package/.gitattributes
ADDED
package/.github/workflows/ci.yml
CHANGED
package/.taprc
CHANGED
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
|
|
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/
|
|
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://
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
417
|
-
|
|
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
|
-
* [
|
|
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 {
|
|
4
|
+
const { LruMap } = require('toad-cache')
|
|
5
5
|
const querystring = require('fast-querystring')
|
|
6
|
-
const
|
|
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 :
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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 (
|
|
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 ${
|
|
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
|
-
|
|
144
|
-
|
|
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
|
|
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
|
|
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: '
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
259
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
275
|
+
if (http2Opts.sessionTimeout === undefined) {
|
|
263
276
|
http2Opts.sessionTimeout = opts.sessionTimeout || 60000
|
|
264
277
|
}
|
|
265
|
-
if (
|
|
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,
|