@fastify/reply-from 8.3.1 → 8.4.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.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @fastify/reply-from
2
2
 
3
- [![CI](https://github.com/fastify/reply-fro/workflows/CI/badge.svg)](https://github.com/fastify/reply-fro/actions/workflows/ci.yml)
3
+ [![CI](https://github.com/fastify/fastify-reply-from/workflows/CI/badge.svg)](https://github.com/fastify/fastify-reply-from/actions/workflows/ci.yml)
4
4
  [![NPM version](https://img.shields.io/npm/v/@fastify/reply-from.svg?style=flat)](https://www.npmjs.com/package/@fastify/reply-from)
5
5
  [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](https://standardjs.com/)
6
6
 
@@ -166,7 +166,7 @@ proxy.register(require('@fastify/reply-from'), {
166
166
  sessionOptions: { // HTTP/2 session connect options, pass in any options from https://nodejs.org/api/http2.html#http2_http2_connect_authority_options_listener
167
167
  rejectUnauthorized: true
168
168
  },
169
- requestTimeout: { // HTTP/2 request options, pass in any options from https://nodejs.org/api/http2.html#http2_clienthttp2session_request_headers_options
169
+ requestOptions: { // HTTP/2 request options, pass in any options from https://nodejs.org/api/http2.html#clienthttp2sessionrequestheaders-options
170
170
  endStream: true
171
171
  }
172
172
  }
@@ -216,6 +216,34 @@ By default: `['GET', 'HEAD', 'OPTIONS', 'TRACE' ]`
216
216
 
217
217
  This plugin will always retry on 503 errors, _unless_ `retryMethods` does not contain `GET`.
218
218
 
219
+ #### `globalAgent`
220
+
221
+ Enables the possibility to explictly opt-in for global agents.
222
+
223
+ Usage for undici global agent:
224
+ ```js
225
+ import { setGlobalDispatcher, ProxyAgent } from 'undici'
226
+
227
+ const proxyAgent = new ProxyAgent('my.proxy.server')
228
+ setGlobalDispatcher(proxyAgent)
229
+
230
+ fastify.register(FastifyReplyFrom, {
231
+ base: 'http://localhost:3001/',
232
+ globalAgent: true
233
+ })
234
+ ```
235
+
236
+ Usage for http/https global agent:
237
+ ```js
238
+ fastify.register(FastifyReplyFrom, {
239
+ base: 'http://localhost:3001/',
240
+ // http and https is allowed to use http.globalAgent or https.globalAgent
241
+ globalAgent: true,
242
+ http: {
243
+ }
244
+ })
245
+ ```
246
+
219
247
  ---
220
248
 
221
249
  #### `maxRetriesOn503`
package/index.js CHANGED
@@ -37,7 +37,8 @@ const fastifyReplyFrom = fp(function from (fastify, opts, next) {
37
37
  http: opts.http,
38
38
  http2: opts.http2,
39
39
  base,
40
- undici: opts.undici
40
+ undici: opts.undici,
41
+ globalAgent: opts.globalAgent
41
42
  })
42
43
  if (requestBuilt instanceof Error) {
43
44
  next(requestBuilt)
@@ -240,7 +241,9 @@ function onErrorDefault (reply, { error }) {
240
241
  }
241
242
 
242
243
  function isFastifyMultipartRegistered (fastify) {
243
- return fastify.hasContentTypeParser('multipart') && fastify.hasRequestDecorator('multipart')
244
+ // TODO: remove fastify.hasContentTypeParser('multipart') in next major
245
+ // It is used to be compatible with @fastify/multipart@<=7.3.0
246
+ return (fastify.hasContentTypeParser('multipart') || fastify.hasContentTypeParser('multipart/form-data')) && fastify.hasRequestDecorator('multipart')
244
247
  }
245
248
 
246
249
  function createRequestRetry (requestImpl, reply, retriesCount, retryOnError, maxRetriesOn503) {
package/lib/request.js CHANGED
@@ -5,7 +5,7 @@ const querystring = require('querystring')
5
5
  const eos = require('end-of-stream')
6
6
  const pump = require('pump')
7
7
  const undici = require('undici')
8
- const { stripHttp1ConnectionHeaders } = require('./utils')
8
+ const { patchUndiciHeaders, stripHttp1ConnectionHeaders } = require('./utils')
9
9
  const http2 = require('http2')
10
10
 
11
11
  const {
@@ -41,6 +41,7 @@ function buildRequest (opts) {
41
41
  const httpOpts = getHttpOpts(opts)
42
42
  const baseUrl = opts.base && new URL(opts.base).origin
43
43
  const undiciOpts = opts.undici || {}
44
+ const globalAgent = opts.globalAgent
44
45
  let http2Client
45
46
  let undiciAgent
46
47
  let undiciInstance
@@ -51,11 +52,16 @@ function buildRequest (opts) {
51
52
  if (opts.base.startsWith('unix+')) {
52
53
  return new Error('Unix socket destination is not supported when http2 is true')
53
54
  }
54
- } else {
55
+ } else if (!globalAgent) {
55
56
  agents = httpOpts.agents || {
56
57
  'http:': new http.Agent(httpOpts.agentOptions),
57
58
  'https:': new https.Agent(httpOpts.agentOptions)
58
59
  }
60
+ } else {
61
+ agents = {
62
+ 'http:': http.globalAgent,
63
+ 'https:': https.globalAgent
64
+ }
59
65
  }
60
66
 
61
67
  if (isHttp2) {
@@ -68,8 +74,10 @@ function buildRequest (opts) {
68
74
  undiciInstance = new undici.Pool(protocol + '://localhost', undiciOpts)
69
75
  } else if (isUndiciInstance(opts.undici)) {
70
76
  undiciInstance = opts.undici
71
- } else {
77
+ } else if (!globalAgent) {
72
78
  undiciAgent = new undici.Agent(getUndiciOptions(opts.undici))
79
+ } else {
80
+ undiciAgent = undici.getGlobalDispatcher()
73
81
  }
74
82
  return { request: handleUndici, close, retryOnError: 'UND_ERR_SOCKET' }
75
83
  } else {
@@ -77,6 +85,10 @@ function buildRequest (opts) {
77
85
  }
78
86
 
79
87
  function close () {
88
+ if (globalAgent) {
89
+ return
90
+ }
91
+
80
92
  if (hasUndiciOptions) {
81
93
  undiciAgent && undiciAgent.destroy()
82
94
  undiciInstance && undiciInstance.destroy()
@@ -146,7 +158,7 @@ function buildRequest (opts) {
146
158
  // using delete, otherwise it will render as an empty string
147
159
  delete res.headers['transfer-encoding']
148
160
 
149
- done(null, { statusCode: res.statusCode, headers: res.headers, stream: res.body })
161
+ done(null, { statusCode: res.statusCode, headers: patchUndiciHeaders(res.headers), stream: res.body })
150
162
  })
151
163
  }
152
164
 
package/lib/utils.js CHANGED
@@ -12,6 +12,31 @@ function filterPseudoHeaders (headers) {
12
12
  return dest
13
13
  }
14
14
 
15
+ // http requires the header to be encoded with latin1
16
+ // undici will convert the latin1 buffer to utf8
17
+ // https://github.com/nodejs/undici/blob/2b260c997ad4efe4ed2064b264b4b546a59e7a67/lib/core/util.js#L216-L229
18
+ // after chaining, the header will be serialised using wrong encoding
19
+ // Buffer.from('', 'latin1').toString('utf8') applied
20
+ //
21
+ // in order to persist the encoding, always encode it
22
+ // back to latin1
23
+ function patchUndiciHeaders (headers) {
24
+ const headersKeys = Object.keys(headers)
25
+ const dist = {}
26
+
27
+ let header
28
+ let i
29
+
30
+ for (i = 0; i < headersKeys.length; i++) {
31
+ header = headersKeys[i]
32
+ if (header.charCodeAt(0) !== 58) { // fast path for indexOf(':') === 0
33
+ dist[header] = Buffer.from(headers[header]).toString('latin1')
34
+ }
35
+ }
36
+
37
+ return dist
38
+ }
39
+
15
40
  function copyHeaders (headers, reply) {
16
41
  const headersKeys = Object.keys(headers)
17
42
 
@@ -74,6 +99,7 @@ function buildURL (source, reqBase) {
74
99
  }
75
100
 
76
101
  module.exports = {
102
+ patchUndiciHeaders,
77
103
  copyHeaders,
78
104
  stripHttp1ConnectionHeaders,
79
105
  filterPseudoHeaders,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fastify/reply-from",
3
- "version": "8.3.1",
3
+ "version": "8.4.1",
4
4
  "description": "forward your HTTP request to another server, for fastify",
5
5
  "main": "index.js",
6
6
  "types": "types/index.d.ts",
@@ -28,8 +28,8 @@
28
28
  },
29
29
  "homepage": "https://github.com/fastify/fastify-reply-from#readme",
30
30
  "devDependencies": {
31
- "@fastify/formbody": "^7.0.1",
32
- "@fastify/multipart": "^7.1.0",
31
+ "@fastify/formbody": "^7.4.0",
32
+ "@fastify/multipart": "^7.4.0",
33
33
  "@fastify/pre-commit": "^2.0.2",
34
34
  "@sinonjs/fake-timers": "^10.0.0",
35
35
  "@types/node": "^18.0.0",
@@ -46,7 +46,7 @@
46
46
  "split2": "^4.1.0",
47
47
  "standard": "^17.0.0",
48
48
  "tap": "^16.2.0",
49
- "tsd": "^0.24.1"
49
+ "tsd": "^0.25.0"
50
50
  },
51
51
  "dependencies": {
52
52
  "@fastify/error": "^3.0.0",
@@ -0,0 +1,57 @@
1
+ 'use strict'
2
+
3
+ const { test } = require('tap')
4
+ const Fastify = require('fastify')
5
+ const From = require('..')
6
+ const http = require('http')
7
+ const get = require('simple-get').concat
8
+
9
+ test('http global agent is used, but not destroyed', async (t) => {
10
+ http.globalAgent.destroy = () => {
11
+ t.fail()
12
+ }
13
+ const instance = Fastify()
14
+ t.teardown(instance.close.bind(instance))
15
+ instance.get('/', (request, reply) => {
16
+ reply.from()
17
+ })
18
+
19
+ const target = http.createServer((req, res) => {
20
+ t.pass('request proxied')
21
+ t.equal(req.method, 'GET')
22
+ t.equal(req.url, '/')
23
+ res.statusCode = 200
24
+ res.end()
25
+ })
26
+ t.teardown(target.close.bind(target))
27
+
28
+ const executionFlow = () => new Promise((resolve) => {
29
+ target.listen({ port: 0 }, (err) => {
30
+ t.error(err)
31
+
32
+ instance.register(From, {
33
+ base: `http://localhost:${target.address().port}`,
34
+ globalAgent: true,
35
+ http: {
36
+ }
37
+ })
38
+
39
+ instance.listen({ port: 0 }, (err) => {
40
+ t.error(err)
41
+
42
+ get(
43
+ `http://localhost:${instance.server.address().port}`,
44
+ (err, res) => {
45
+ t.error(err)
46
+ t.equal(res.statusCode, 200)
47
+ resolve()
48
+ }
49
+ )
50
+ })
51
+ })
52
+ })
53
+
54
+ await executionFlow()
55
+
56
+ target.close()
57
+ })
@@ -0,0 +1,69 @@
1
+ 'use strict'
2
+
3
+ const { test } = require('tap')
4
+ const Fastify = require('fastify')
5
+ const From = require('..')
6
+ const https = require('https')
7
+ const get = require('simple-get').concat
8
+
9
+ const fs = require('fs')
10
+ const path = require('path')
11
+ const certs = {
12
+ key: fs.readFileSync(path.join(__dirname, 'fixtures', 'fastify.key')),
13
+ cert: fs.readFileSync(path.join(__dirname, 'fixtures', 'fastify.cert'))
14
+ }
15
+
16
+ test('https global agent is used, but not destroyed', async (t) => {
17
+ https.globalAgent.destroy = () => {
18
+ t.fail()
19
+ }
20
+ const instance = Fastify({
21
+ https: certs
22
+ })
23
+ t.teardown(instance.close.bind(instance))
24
+ instance.get('/', (request, reply) => {
25
+ reply.from()
26
+ })
27
+
28
+ const target = https.createServer(certs, (req, res) => {
29
+ t.pass('request proxied')
30
+ t.equal(req.method, 'GET')
31
+ t.equal(req.url, '/')
32
+ res.statusCode = 200
33
+ res.end()
34
+ })
35
+ t.teardown(target.close.bind(target))
36
+
37
+ const executionFlow = () => new Promise((resolve) => {
38
+ target.listen({ port: 0 }, (err) => {
39
+ t.error(err)
40
+
41
+ instance.register(From, {
42
+ base: `https://localhost:${target.address().port}`,
43
+ globalAgent: true,
44
+ http: {
45
+ }
46
+ })
47
+
48
+ instance.listen({ port: 0 }, (err) => {
49
+ t.error(err)
50
+
51
+ get(
52
+ {
53
+ url: `https://localhost:${instance.server.address().port}`,
54
+ rejectUnauthorized: false
55
+ },
56
+ (err, res) => {
57
+ t.error(err)
58
+ t.equal(res.statusCode, 200)
59
+ resolve()
60
+ }
61
+ )
62
+ })
63
+ })
64
+ })
65
+
66
+ await executionFlow()
67
+
68
+ target.close()
69
+ })
@@ -0,0 +1,57 @@
1
+ 'use strict'
2
+
3
+ const t = require('tap')
4
+ const Fastify = require('fastify')
5
+ const get = require('simple-get').concat
6
+ const From = require('..')
7
+
8
+ const header = 'attachment; filename="år.pdf"'
9
+
10
+ t.plan(6)
11
+
12
+ const instance = Fastify()
13
+ t.teardown(instance.close.bind(instance))
14
+ const proxy1 = Fastify()
15
+ t.teardown(proxy1.close.bind(proxy1))
16
+ const proxy2 = Fastify()
17
+ t.teardown(proxy2.close.bind(proxy2))
18
+
19
+ instance.get('/', (request, reply) => {
20
+ reply.header('content-disposition', header).send('OK')
21
+ })
22
+
23
+ proxy1.register(From, {
24
+ undici: {
25
+ keepAliveMaxTimeout: 10
26
+ }
27
+ })
28
+ proxy1.get('/', (request, reply) => {
29
+ return reply.from(`http://localhost:${instance.server.address().port}`)
30
+ })
31
+
32
+ proxy2.register(From, {
33
+ undici: {
34
+ keepAliveMaxTimeout: 10
35
+ }
36
+ })
37
+ proxy2.get('/', (request, reply) => {
38
+ return reply.from(`http://localhost:${proxy1.server.address().port}`)
39
+ })
40
+
41
+ instance.listen({ port: 0 }, err => {
42
+ t.error(err)
43
+
44
+ proxy1.listen({ port: 0 }, err => {
45
+ t.error(err)
46
+
47
+ proxy2.listen({ port: 0 }, err => {
48
+ t.error(err)
49
+
50
+ get(`http://localhost:${proxy2.server.address().port}`, (err, res, data) => {
51
+ t.error(err)
52
+ t.equal(res.statusCode, 200)
53
+ t.equal(data.toString(), 'OK')
54
+ })
55
+ })
56
+ })
57
+ })
@@ -0,0 +1,66 @@
1
+ 'use strict'
2
+
3
+ const { test } = require('tap')
4
+ const Fastify = require('fastify')
5
+ const http = require('http')
6
+ const get = require('simple-get').concat
7
+ const undici = require('undici')
8
+ const From = require('..')
9
+
10
+ test('undici global agent is used, but not destroyed', async (t) => {
11
+ const mockAgent = new undici.Agent()
12
+ mockAgent.destroy = () => {
13
+ t.fail()
14
+ }
15
+ undici.setGlobalDispatcher(mockAgent)
16
+ const instance = Fastify()
17
+
18
+ t.teardown(instance.close.bind(instance))
19
+
20
+ const target = http.createServer((req, res) => {
21
+ res.statusCode = 200
22
+ res.end()
23
+ })
24
+
25
+ instance.get('/', (request, reply) => {
26
+ reply.from()
27
+ })
28
+
29
+ t.teardown(target.close.bind(target))
30
+
31
+ const executionFlow = () => new Promise((resolve) => {
32
+ target.listen({ port: 0 }, (err) => {
33
+ t.error(err)
34
+
35
+ instance.register(From, {
36
+ base: `http://localhost:${target.address().port}`,
37
+ globalAgent: true
38
+ })
39
+
40
+ instance.listen({ port: 0 }, (err) => {
41
+ t.error(err)
42
+
43
+ get(
44
+ `http://localhost:${instance.server.address().port}`,
45
+ (err, res) => {
46
+ t.error(err)
47
+ t.equal(res.statusCode, 200)
48
+
49
+ get(
50
+ `http://localhost:${instance.server.address().port}`,
51
+ (err, res) => {
52
+ t.error(err)
53
+ t.equal(res.statusCode, 200)
54
+ resolve()
55
+ }
56
+ )
57
+ }
58
+ )
59
+ })
60
+ })
61
+ })
62
+
63
+ await executionFlow()
64
+
65
+ target.close()
66
+ })
package/types/index.d.ts CHANGED
@@ -95,6 +95,7 @@ declare namespace fastifyReplyFrom {
95
95
  retryMethods?: (HTTPMethods | 'TRACE')[];
96
96
  maxRetriesOn503?: number;
97
97
  disableRequestLogging?: boolean;
98
+ globalAgent?: boolean;
98
99
  }
99
100
 
100
101
  export const fastifyReplyFrom: FastifyReplyFrom
@@ -45,6 +45,7 @@ const fullOptions: FastifyReplyFromOptions = {
45
45
  retryMethods: ['GET', 'HEAD', 'OPTIONS', 'TRACE'],
46
46
  maxRetriesOn503: 10,
47
47
  disableRequestLogging: false,
48
+ globalAgent: false,
48
49
  };
49
50
  tap.autoend(false);
50
51