@fastify/reply-from 7.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 (88) hide show
  1. package/.github/dependabot.yml +13 -0
  2. package/.github/stale.yml +21 -0
  3. package/.github/workflows/ci.yml +60 -0
  4. package/.taprc +5 -0
  5. package/LICENSE +21 -0
  6. package/README.md +335 -0
  7. package/example.js +36 -0
  8. package/index.d.ts +99 -0
  9. package/index.js +262 -0
  10. package/lib/request.js +288 -0
  11. package/lib/utils.js +81 -0
  12. package/package.json +70 -0
  13. package/test/async-route-handler.js +50 -0
  14. package/test/base-get.js +48 -0
  15. package/test/base-path.js +38 -0
  16. package/test/base-querystring.js +48 -0
  17. package/test/build-url.js +69 -0
  18. package/test/core-with-path-in-base.js +50 -0
  19. package/test/custom-undici-instance.js +77 -0
  20. package/test/fastify-multipart-incompatibility.js +90 -0
  21. package/test/fixtures/fastify.cert +19 -0
  22. package/test/fixtures/fastify.key +27 -0
  23. package/test/fixtures/file.txt +1 -0
  24. package/test/full-get.js +45 -0
  25. package/test/full-https-get.js +55 -0
  26. package/test/full-post-extended-content-type.js +59 -0
  27. package/test/full-post-http2.js +49 -0
  28. package/test/full-post-stream-core.js +66 -0
  29. package/test/full-post-stream.js +64 -0
  30. package/test/full-post.js +56 -0
  31. package/test/full-querystring-rewrite-option-complex.js +48 -0
  32. package/test/full-querystring-rewrite-option-function.js +51 -0
  33. package/test/full-querystring-rewrite-option.js +48 -0
  34. package/test/full-querystring-rewrite-string.js +46 -0
  35. package/test/full-querystring-rewrite.js +46 -0
  36. package/test/full-querystring.js +46 -0
  37. package/test/full-rewrite-body-content-type.js +59 -0
  38. package/test/full-rewrite-body-http.js +63 -0
  39. package/test/full-rewrite-body-to-empty-string.js +61 -0
  40. package/test/full-rewrite-body-to-null.js +61 -0
  41. package/test/full-rewrite-body.js +61 -0
  42. package/test/get-upstream-http.js +70 -0
  43. package/test/get-upstream-undici.js +45 -0
  44. package/test/get-with-body.js +55 -0
  45. package/test/head-with-body.js +57 -0
  46. package/test/host-header.js +67 -0
  47. package/test/http-agents.js +55 -0
  48. package/test/http-http2.js +50 -0
  49. package/test/http-invalid-target.js +35 -0
  50. package/test/http-retry.js +109 -0
  51. package/test/http-timeout.js +56 -0
  52. package/test/http2-http2.js +57 -0
  53. package/test/http2-https.js +80 -0
  54. package/test/http2-invalid-target.js +36 -0
  55. package/test/http2-target-crash.js +53 -0
  56. package/test/http2-target-multi-crash.js +60 -0
  57. package/test/http2-timeout.js +92 -0
  58. package/test/https-agents.js +67 -0
  59. package/test/index.test-d.ts +136 -0
  60. package/test/modifyCoreObjects-false.js +48 -0
  61. package/test/no-body-opts-with-get.js +49 -0
  62. package/test/no-body-opts-with-head.js +51 -0
  63. package/test/no-stream-body-option.js +58 -0
  64. package/test/on-error.js +62 -0
  65. package/test/onResponse.js +48 -0
  66. package/test/padded-body.js +64 -0
  67. package/test/post-formbody.js +59 -0
  68. package/test/post-plain-text.js +56 -0
  69. package/test/post-with-custom-encoded-contenttype.js +65 -0
  70. package/test/retry-on-503.js +101 -0
  71. package/test/rewrite-headers.js +52 -0
  72. package/test/rewrite-request-headers.js +47 -0
  73. package/test/transform-body.js +61 -0
  74. package/test/undici-agent.js +77 -0
  75. package/test/undici-body.js +70 -0
  76. package/test/undici-options.js +73 -0
  77. package/test/undici-retry.js +109 -0
  78. package/test/undici-timeout-body-partial.js +60 -0
  79. package/test/undici-timeout-body.js +63 -0
  80. package/test/undici-timeout.js +61 -0
  81. package/test/undici-with-path-in-base.js +50 -0
  82. package/test/undici.js +50 -0
  83. package/test/unexpected-error.js +46 -0
  84. package/test/unix-http-undici-from.js +51 -0
  85. package/test/unix-http-undici.js +62 -0
  86. package/test/unix-http.js +65 -0
  87. package/test/unix-https-undici.js +71 -0
  88. package/test/unix-https.js +71 -0
package/index.js ADDED
@@ -0,0 +1,262 @@
1
+ 'use strict'
2
+
3
+ const fp = require('fastify-plugin')
4
+ const lru = require('tiny-lru')
5
+ const querystring = require('querystring')
6
+ const Stream = require('stream')
7
+ const createError = require('http-errors')
8
+ const buildRequest = require('./lib/request')
9
+ const {
10
+ filterPseudoHeaders,
11
+ copyHeaders,
12
+ stripHttp1ConnectionHeaders,
13
+ buildURL
14
+ } = require('./lib/utils')
15
+
16
+ const { TimeoutError } = buildRequest
17
+
18
+ module.exports = fp(function from (fastify, opts, next) {
19
+ const contentTypesToEncode = new Set([
20
+ 'application/json',
21
+ ...(opts.contentTypesToEncode || [])
22
+ ])
23
+
24
+ const retryMethods = new Set(opts.retryMethods || [
25
+ 'GET', 'HEAD', 'OPTIONS', 'TRACE'
26
+ ])
27
+
28
+ const cache = opts.disableCache ? undefined : lru(opts.cacheURLs || 100)
29
+ const base = opts.base
30
+ const { request, close, retryOnError } = buildRequest({
31
+ http: opts.http,
32
+ http2: opts.http2,
33
+ base,
34
+ undici: opts.undici
35
+ })
36
+
37
+ fastify.decorateReply('from', function (source, opts) {
38
+ opts = opts || {}
39
+ const req = this.request.raw
40
+ const onResponse = opts.onResponse
41
+ const rewriteHeaders = opts.rewriteHeaders || headersNoOp
42
+ const rewriteRequestHeaders = opts.rewriteRequestHeaders || requestHeadersNoOp
43
+ const getUpstream = opts.getUpstream || upstreamNoOp
44
+ const onError = opts.onError || onErrorDefault
45
+ const retriesCount = opts.retriesCount || 0
46
+ const maxRetriesOn503 = opts.maxRetriesOn503 || 10
47
+
48
+ if (!source) {
49
+ source = req.url
50
+ }
51
+
52
+ // we leverage caching to avoid parsing the destination URL
53
+ const dest = getUpstream(req, base)
54
+ let url
55
+ if (cache) {
56
+ url = cache.get(source) || buildURL(source, dest)
57
+ cache.set(source, url)
58
+ } else {
59
+ url = buildURL(source, dest)
60
+ }
61
+
62
+ const sourceHttp2 = req.httpVersionMajor === 2
63
+ const headers = sourceHttp2 ? filterPseudoHeaders(req.headers) : req.headers
64
+ headers.host = url.host
65
+ const qs = getQueryString(url.search, req.url, opts)
66
+ let body = ''
67
+
68
+ if (opts.body !== undefined) {
69
+ if (opts.body !== null) {
70
+ if (typeof opts.body.pipe === 'function') {
71
+ throw new Error('sending a new body as a stream is not supported yet')
72
+ }
73
+
74
+ if (opts.contentType) {
75
+ body = opts.body
76
+ } else {
77
+ body = JSON.stringify(opts.body)
78
+ opts.contentType = 'application/json'
79
+ }
80
+
81
+ headers['content-length'] = Buffer.byteLength(body)
82
+ headers['content-type'] = opts.contentType
83
+ } else {
84
+ body = undefined
85
+ headers['content-length'] = 0
86
+ delete headers['content-type']
87
+ }
88
+ } else if (this.request.body) {
89
+ if (this.request.body instanceof Stream) {
90
+ body = this.request.body
91
+ } else {
92
+ // Per RFC 7231 §3.1.1.5 if this header is not present we MAY assume application/octet-stream
93
+ const contentType = req.headers['content-type'] || 'application/octet-stream'
94
+ // detect if body should be encoded as JSON
95
+ // supporting extended content-type header formats:
96
+ // - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type
97
+ const lowerCaseContentType = contentType.toLowerCase()
98
+ const plainContentType = lowerCaseContentType.indexOf(';') > -1
99
+ ? lowerCaseContentType.slice(0, lowerCaseContentType.indexOf(';'))
100
+ : lowerCaseContentType
101
+ const shouldEncodeJSON = contentTypesToEncode.has(plainContentType)
102
+ // transparently support JSON encoding
103
+ body = shouldEncodeJSON ? JSON.stringify(this.request.body) : this.request.body
104
+ // update origin request headers after encoding
105
+ headers['content-length'] = Buffer.byteLength(body)
106
+ headers['content-type'] = contentType
107
+ }
108
+ }
109
+
110
+ // according to https://tools.ietf.org/html/rfc2616#section-4.3
111
+ // fastify ignore message body when it's a GET or HEAD request
112
+ // when proxy this request, we should reset the content-length to make it a valid http request
113
+ // discussion: https://github.com/fastify/fastify/issues/953
114
+ if (req.method === 'GET' || req.method === 'HEAD') {
115
+ // body will be populated here only if opts.body is passed.
116
+ // if we are doing that with a GET or HEAD request is a programmer error
117
+ // and as such we can throw immediately.
118
+ if (body) {
119
+ throw new Error(`Rewriting the body when doing a ${req.method} is not allowed`)
120
+ }
121
+ }
122
+
123
+ this.request.log.info({ source }, 'fetching from remote server')
124
+
125
+ const requestHeaders = rewriteRequestHeaders(req, headers)
126
+ const contentLength = requestHeaders['content-length']
127
+ let requestImpl
128
+ if (retryMethods.has(req.method) && !contentLength) {
129
+ requestImpl = createRequestRetry(request, this, retriesCount, retryOnError, maxRetriesOn503)
130
+ } else {
131
+ requestImpl = request
132
+ }
133
+
134
+ requestImpl({ method: req.method, url, qs, headers: requestHeaders, body }, (err, res) => {
135
+ if (err) {
136
+ this.request.log.warn(err, 'response errored')
137
+ if (!this.sent) {
138
+ if (err.code === 'ERR_HTTP2_STREAM_CANCEL' || err.code === 'ENOTFOUND') {
139
+ onError(this, { error: new createError.ServiceUnavailable() })
140
+ } else if (err instanceof TimeoutError || err.code === 'UND_ERR_HEADERS_TIMEOUT') {
141
+ onError(this, { error: new createError.GatewayTimeout() })
142
+ } else {
143
+ onError(this, { error: createError(500, err) })
144
+ }
145
+ }
146
+ return
147
+ }
148
+ this.request.log.info('response received')
149
+ if (sourceHttp2) {
150
+ copyHeaders(
151
+ rewriteHeaders(stripHttp1ConnectionHeaders(res.headers), req),
152
+ this
153
+ )
154
+ } else {
155
+ copyHeaders(rewriteHeaders(res.headers, req), this)
156
+ }
157
+ this.code(res.statusCode)
158
+ if (onResponse) {
159
+ onResponse(this.request, this, res.stream)
160
+ } else {
161
+ this.send(res.stream)
162
+ }
163
+ })
164
+ return this
165
+ })
166
+
167
+ fastify.addHook('onReady', (done) => {
168
+ if (isFastifyMultipartRegistered(fastify)) {
169
+ fastify.log.warn('fastify-reply-from might not behave as expected when used with fastify-multipart')
170
+ }
171
+ done()
172
+ })
173
+
174
+ fastify.onClose((fastify, next) => {
175
+ close()
176
+ // let the event loop do a full run so that it can
177
+ // actually destroy those sockets
178
+ setImmediate(next)
179
+ })
180
+
181
+ next()
182
+ }, '>=3')
183
+
184
+ function getQueryString (search, reqUrl, opts) {
185
+ if (typeof opts.queryString === 'function') {
186
+ return '?' + opts.queryString(search, reqUrl)
187
+ }
188
+
189
+ if (opts.queryString) {
190
+ return '?' + querystring.stringify(opts.queryString)
191
+ }
192
+
193
+ if (search.length > 0) {
194
+ return search
195
+ }
196
+
197
+ const queryIndex = reqUrl.indexOf('?')
198
+
199
+ if (queryIndex > 0) {
200
+ return reqUrl.slice(queryIndex)
201
+ }
202
+
203
+ return ''
204
+ }
205
+
206
+ function headersNoOp (headers, originalReq) {
207
+ return headers
208
+ }
209
+
210
+ function requestHeadersNoOp (originalReq, headers) {
211
+ return headers
212
+ }
213
+
214
+ function upstreamNoOp (req, base) {
215
+ return base
216
+ }
217
+
218
+ function onErrorDefault (reply, { error }) {
219
+ reply.send(error)
220
+ }
221
+
222
+ function isFastifyMultipartRegistered (fastify) {
223
+ return fastify.hasContentTypeParser('multipart') && fastify.hasRequestDecorator('multipart')
224
+ }
225
+
226
+ function createRequestRetry (requestImpl, reply, retriesCount, retryOnError, maxRetriesOn503) {
227
+ function requestRetry (req, cb) {
228
+ let retries = 0
229
+
230
+ function run () {
231
+ requestImpl(req, function (err, res) {
232
+ // Magic number, so why not 42? We might want to make this configurable.
233
+ let retryAfter = 42 * Math.random() * (retries + 1)
234
+
235
+ if (res && res.headers['retry-after']) {
236
+ retryAfter = res.headers['retry-after']
237
+ }
238
+ if (!reply.sent) {
239
+ // always retry on 503 errors
240
+ if (res && res.statusCode === 503 && req.method === 'GET') {
241
+ if (retriesCount === 0 && retries < maxRetriesOn503) {
242
+ // we should stop at some point
243
+ return retry(retryAfter)
244
+ }
245
+ } else if (retriesCount > retries && err && err.code === retryOnError) {
246
+ return retry(retryAfter)
247
+ }
248
+ }
249
+ cb(err, res)
250
+ })
251
+ }
252
+
253
+ function retry (after) {
254
+ retries += 1
255
+ setTimeout(run, after)
256
+ }
257
+
258
+ run()
259
+ }
260
+
261
+ return requestRetry
262
+ }
package/lib/request.js ADDED
@@ -0,0 +1,288 @@
1
+ 'use strict'
2
+ const semver = require('semver')
3
+ const http = require('http')
4
+ const https = require('https')
5
+ const querystring = require('querystring')
6
+ const eos = require('end-of-stream')
7
+ const pump = require('pump')
8
+ const undici = require('undici')
9
+ const { stripHttp1ConnectionHeaders } = require('./utils')
10
+ const http2 = require('http2')
11
+
12
+ class TimeoutError extends Error {}
13
+
14
+ function shouldUseUndici (opts) {
15
+ if (opts.undici === false || opts.http || opts.http2) {
16
+ return false
17
+ }
18
+ return true
19
+ }
20
+
21
+ function isUndiciInstance (obj) {
22
+ return obj instanceof undici.Pool ||
23
+ obj instanceof undici.Client ||
24
+ obj instanceof undici.Dispatcher
25
+ }
26
+
27
+ function buildRequest (opts) {
28
+ const isHttp2 = !!opts.http2
29
+ const hasUndiciOptions = shouldUseUndici(opts)
30
+ const requests = {
31
+ 'http:': http,
32
+ 'https:': https,
33
+ 'unix+http:': { base: http, request: unixRequest },
34
+ 'unix+https:': { base: https, request: unixRequest }
35
+ }
36
+ const http2Opts = getHttp2Opts(opts)
37
+ const httpOpts = getHttpOpts(opts)
38
+ const baseUrl = opts.base && new URL(opts.base).origin
39
+ const undiciOpts = opts.undici || {}
40
+ let http2Client
41
+ let undiciAgent
42
+ let undiciInstance
43
+ let agents
44
+
45
+ if (isHttp2) {
46
+ if (semver.lt(process.version, '9.0.0')) {
47
+ throw new Error('Http2 support requires Node version >= 9.0.0')
48
+ }
49
+ if (!opts.base) throw new Error('Option base is required when http2 is true')
50
+ if (opts.base.startsWith('unix+')) {
51
+ throw new Error('Unix socket destination is not supported when http2 is true')
52
+ }
53
+ } else {
54
+ agents = httpOpts.agents || {
55
+ 'http:': new http.Agent(httpOpts.agentOptions),
56
+ 'https:': new https.Agent(httpOpts.agentOptions)
57
+ }
58
+ }
59
+
60
+ if (isHttp2) {
61
+ return { request: handleHttp2Req, close, retryOnError: 'ECONNRESET' }
62
+ } else if (hasUndiciOptions) {
63
+ if (opts.base && opts.base.startsWith('unix+')) {
64
+ const undiciOpts = getUndiciOptions(opts.undici)
65
+ undiciOpts.socketPath = decodeURIComponent(new URL(opts.base).host)
66
+ const protocol = opts.base.startsWith('unix+https') ? 'https' : 'http'
67
+ undiciInstance = new undici.Pool(protocol + '://localhost', undiciOpts)
68
+ } else if (isUndiciInstance(opts.undici)) {
69
+ undiciInstance = opts.undici
70
+ } else {
71
+ undiciAgent = new undici.Agent(getUndiciOptions(opts.undici))
72
+ }
73
+ return { request: handleUndici, close, retryOnError: 'UND_ERR_SOCKET' }
74
+ } else {
75
+ return { request: handleHttp1Req, close, retryOnError: 'ECONNRESET' }
76
+ }
77
+
78
+ function close () {
79
+ if (hasUndiciOptions) {
80
+ undiciAgent && undiciAgent.destroy()
81
+ undiciInstance && undiciInstance.destroy()
82
+ } else if (!isHttp2) {
83
+ agents['http:'].destroy()
84
+ agents['https:'].destroy()
85
+ } else if (http2Client) {
86
+ http2Client.destroy()
87
+ }
88
+ }
89
+
90
+ function handleHttp1Req (opts, done) {
91
+ const req = requests[opts.url.protocol].request({
92
+ method: opts.method,
93
+ port: opts.url.port,
94
+ path: opts.url.pathname + opts.qs,
95
+ hostname: opts.url.hostname,
96
+ headers: opts.headers,
97
+ agent: agents[opts.url.protocol.replace(/^unix:/, '')],
98
+ ...httpOpts.requestOptions
99
+ })
100
+ req.on('error', done)
101
+ req.on('response', res => {
102
+ done(null, { statusCode: res.statusCode, headers: res.headers, stream: res })
103
+ })
104
+ req.once('timeout', () => {
105
+ const err = new TimeoutError('HTTP request timed out')
106
+ req.abort()
107
+ done(err)
108
+ })
109
+
110
+ end(req, opts.body, done)
111
+ }
112
+
113
+ function handleUndici (opts, done) {
114
+ const req = {
115
+ origin: baseUrl || opts.url.origin,
116
+ path: opts.url.pathname + opts.qs,
117
+ method: opts.method,
118
+ headers: Object.assign({}, opts.headers),
119
+ body: opts.body,
120
+ headersTimeout: undiciOpts.headersTimeout,
121
+ bodyTimeout: undiciOpts.bodyTimeout
122
+ }
123
+
124
+ let pool
125
+
126
+ if (undiciInstance) {
127
+ pool = undiciInstance
128
+ } else if (!baseUrl && opts.url.protocol.startsWith('unix')) {
129
+ done(new Error('unix socket not supported with undici yet'))
130
+ return
131
+ } else {
132
+ pool = undiciAgent
133
+ }
134
+
135
+ // remove forbidden headers
136
+ req.headers.connection = undefined
137
+ req.headers['transfer-encoding'] = undefined
138
+
139
+ pool.request(req, function (err, res) {
140
+ if (err) {
141
+ done(err)
142
+ return
143
+ }
144
+
145
+ // using delete, otherwise it will render as an empty string
146
+ delete res.headers['transfer-encoding']
147
+
148
+ done(null, { statusCode: res.statusCode, headers: res.headers, stream: res.body })
149
+ })
150
+ }
151
+
152
+ function handleHttp2Req (opts, done) {
153
+ let cancelRequest
154
+ let sessionTimedOut = false
155
+
156
+ if (!http2Client || http2Client.destroyed) {
157
+ http2Client = http2.connect(baseUrl, http2Opts.sessionOptions)
158
+ http2Client.once('error', done)
159
+ // we might enqueue a large number of requests in this connection
160
+ // before it's connected
161
+ http2Client.setMaxListeners(0)
162
+ http2Client.setTimeout(http2Opts.sessionTimeout, function () {
163
+ if (cancelRequest) {
164
+ cancelRequest()
165
+ cancelRequest = undefined
166
+ sessionTimedOut = true
167
+ }
168
+ http2Client.destroy()
169
+ })
170
+ http2Client.once('connect', () => {
171
+ // reset the max listener to 10 on connect
172
+ http2Client.setMaxListeners(10)
173
+ http2Client.removeListener('error', done)
174
+ })
175
+ }
176
+ const req = http2Client.request({
177
+ ':method': opts.method,
178
+ ':path': opts.url.pathname + opts.qs,
179
+ ...stripHttp1ConnectionHeaders(opts.headers)
180
+ }, http2Opts.requestOptions)
181
+ const isGet = opts.method === 'GET' || opts.method === 'get'
182
+ if (!isGet) {
183
+ end(req, opts.body, done)
184
+ }
185
+ req.setTimeout(http2Opts.requestTimeout, () => {
186
+ const err = new TimeoutError('HTTP/2 request timed out')
187
+ req.close(http2.constants.NGHTTP2_CANCEL)
188
+ done(err)
189
+ })
190
+ req.once('close', () => {
191
+ if (sessionTimedOut) {
192
+ const err = new TimeoutError('HTTP/2 session timed out')
193
+ done(err)
194
+ }
195
+ })
196
+ cancelRequest = eos(req, err => {
197
+ if (err) done(err)
198
+ })
199
+ req.on('response', headers => {
200
+ const statusCode = headers[':status']
201
+ done(null, { statusCode, headers, stream: req })
202
+ })
203
+ }
204
+ }
205
+
206
+ module.exports = buildRequest
207
+ module.exports.TimeoutError = TimeoutError
208
+
209
+ function unixRequest (opts) {
210
+ delete opts.port
211
+ opts.socketPath = querystring.unescape(opts.hostname)
212
+ delete opts.hostname
213
+ return this.base.request(opts)
214
+ }
215
+
216
+ function end (req, body, cb) {
217
+ if (!body || typeof body === 'string' || body instanceof Uint8Array) {
218
+ req.end(body)
219
+ } else if (body.pipe) {
220
+ pump(body, req, err => {
221
+ if (err) cb(err)
222
+ })
223
+ } else {
224
+ cb(new Error(`type unsupported for body: ${body.constructor}`))
225
+ }
226
+ }
227
+
228
+ function getHttp2Opts (opts) {
229
+ if (!opts.http2) {
230
+ return {}
231
+ }
232
+
233
+ let http2Opts = opts.http2
234
+ if (typeof http2Opts === 'boolean') {
235
+ http2Opts = {}
236
+ }
237
+ http2Opts.sessionOptions = http2Opts.sessionOptions || {}
238
+
239
+ if (!http2Opts.sessionTimeout) {
240
+ http2Opts.sessionTimeout = opts.sessionTimeout || 6000
241
+ }
242
+ if (!http2Opts.requestTimeout) {
243
+ http2Opts.requestTimeout = 10000
244
+ }
245
+ http2Opts.sessionOptions.rejectUnauthorized = http2Opts.sessionOptions.rejectUnauthorized || false
246
+
247
+ return http2Opts
248
+ }
249
+
250
+ function getHttpOpts (opts) {
251
+ const httpOpts = typeof opts.http === 'object' ? opts.http : {}
252
+ httpOpts.requestOptions = httpOpts.requestOptions || {}
253
+
254
+ if (!httpOpts.requestOptions.timeout) {
255
+ httpOpts.requestOptions.timeout = 10000
256
+ }
257
+
258
+ httpOpts.requestOptions.rejectUnauthorized = httpOpts.requestOptions.rejectUnauthorized || false
259
+
260
+ httpOpts.agentOptions = getAgentOptions(opts)
261
+
262
+ return httpOpts
263
+ }
264
+
265
+ function getAgentOptions (opts) {
266
+ return {
267
+ keepAlive: true,
268
+ keepAliveMsecs: 60 * 1000, // 1 minute
269
+ maxSockets: 2048,
270
+ maxFreeSockets: 2048,
271
+ ...(opts.http && opts.http.agentOptions)
272
+ }
273
+ }
274
+
275
+ function getUndiciOptions (opts = {}) {
276
+ const res = {
277
+ pipelining: 1,
278
+ connections: 128,
279
+ tls: {},
280
+ ...(opts)
281
+ }
282
+
283
+ res.tls.rejectUnauthorized = res.tls.rejectUnauthorized || false
284
+
285
+ return res
286
+ }
287
+
288
+ module.exports.getUndiciOptions = getUndiciOptions
package/lib/utils.js ADDED
@@ -0,0 +1,81 @@
1
+ function filterPseudoHeaders (headers) {
2
+ const dest = {}
3
+ const headersKeys = Object.keys(headers)
4
+ let header
5
+ let i
6
+ for (i = 0; i < headersKeys.length; i++) {
7
+ header = headersKeys[i]
8
+ if (header.charCodeAt(0) !== 58) { // fast path for indexOf(':') === 0
9
+ dest[header.toLowerCase()] = headers[header]
10
+ }
11
+ }
12
+ return dest
13
+ }
14
+
15
+ function copyHeaders (headers, reply) {
16
+ const headersKeys = Object.keys(headers)
17
+
18
+ let header
19
+ let i
20
+
21
+ for (i = 0; i < headersKeys.length; i++) {
22
+ header = headersKeys[i]
23
+ if (header.charCodeAt(0) !== 58) { // fast path for indexOf(':') === 0
24
+ reply.header(header, headers[header])
25
+ }
26
+ }
27
+ }
28
+
29
+ function stripHttp1ConnectionHeaders (headers) {
30
+ const headersKeys = Object.keys(headers)
31
+ const dest = {}
32
+
33
+ let header
34
+ let i
35
+
36
+ for (i = 0; i < headersKeys.length; i++) {
37
+ header = headersKeys[i].toLowerCase()
38
+
39
+ switch (header) {
40
+ case 'connection':
41
+ case 'upgrade':
42
+ case 'http2-settings':
43
+ case 'te':
44
+ case 'transfer-encoding':
45
+ case 'proxy-connection':
46
+ case 'keep-alive':
47
+ case 'host':
48
+ break
49
+ default:
50
+ dest[header] = headers[header]
51
+ break
52
+ }
53
+ }
54
+ return dest
55
+ }
56
+
57
+ // issue ref: https://github.com/fastify/fast-proxy/issues/42
58
+ function buildURL (source, reqBase) {
59
+ let baseOrigin = reqBase ? new URL(reqBase).href : undefined
60
+ const dest = new URL(source, reqBase)
61
+
62
+ // if base is specified, source url should not override it
63
+ if (baseOrigin) {
64
+ if (!baseOrigin.endsWith('/') && dest.href.length > baseOrigin.length) {
65
+ baseOrigin = baseOrigin + '/'
66
+ }
67
+
68
+ if (!dest.href.startsWith(baseOrigin)) {
69
+ throw new Error('source must be a relative path string')
70
+ }
71
+ }
72
+
73
+ return dest
74
+ }
75
+
76
+ module.exports = {
77
+ copyHeaders,
78
+ stripHttp1ConnectionHeaders,
79
+ filterPseudoHeaders,
80
+ buildURL
81
+ }
package/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "@fastify/reply-from",
3
+ "version": "7.0.0",
4
+ "description": "forward your HTTP request to another server, for fastify",
5
+ "main": "index.js",
6
+ "types": "index.d.ts",
7
+ "scripts": {
8
+ "coverage": "tap -j8 test/*.js --cov --coverage-report=html",
9
+ "lint:fix": "standard --fix",
10
+ "test": "standard | snazzy && tap test/*.js && npm run typescript",
11
+ "test:ci": "standard | snazzy && tap test/*.js --coverage-report=lcovonly && npm run typescript",
12
+ "typescript": "tsd"
13
+ },
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/fastify/fastify-reply-from.git"
17
+ },
18
+ "keywords": [
19
+ "fastify",
20
+ "http",
21
+ "forward",
22
+ "proxy"
23
+ ],
24
+ "author": "Matteo Collina <hello@matteocollina.com>",
25
+ "license": "MIT",
26
+ "bugs": {
27
+ "url": "https://github.com/fastify/fastify-reply-from/issues"
28
+ },
29
+ "engines": {
30
+ "node": ">=12.18"
31
+ },
32
+ "homepage": "https://github.com/fastify/fastify-reply-from#readme",
33
+ "devDependencies": {
34
+ "@sinonjs/fake-timers": "^9.0.0",
35
+ "@types/node": "^17.0.0",
36
+ "@types/tap": "^15.0.3",
37
+ "fastify": "^3.17.0",
38
+ "fastify-formbody": "^5.0.0",
39
+ "fastify-multipart": "^5.0.0",
40
+ "form-data": "^4.0.0",
41
+ "got": "^11.8.2",
42
+ "h2url": "^0.2.0",
43
+ "msgpack5": "^6.0.0",
44
+ "nock": "^13.1.0",
45
+ "pre-commit": "^1.2.2",
46
+ "proxyquire": "^2.1.3",
47
+ "simple-get": "^4.0.0",
48
+ "snazzy": "^9.0.0",
49
+ "split2": "^4.0.0",
50
+ "standard": "^17.0.0",
51
+ "tap": "^16.0.0",
52
+ "tsd": "^0.20.0",
53
+ "typescript": "^4.3.2"
54
+ },
55
+ "dependencies": {
56
+ "end-of-stream": "^1.4.4",
57
+ "fastify-plugin": "^3.0.0",
58
+ "http-errors": "^2.0.0",
59
+ "pump": "^3.0.0",
60
+ "semver": "^7.3.5",
61
+ "tiny-lru": "^8.0.1",
62
+ "undici": "^5.0.0"
63
+ },
64
+ "tsd": {
65
+ "directory": "test"
66
+ },
67
+ "publishConfig": {
68
+ "access": "public"
69
+ }
70
+ }