@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.
- package/.github/dependabot.yml +13 -0
- package/.github/stale.yml +21 -0
- package/.github/workflows/ci.yml +60 -0
- package/.taprc +5 -0
- package/LICENSE +21 -0
- package/README.md +335 -0
- package/example.js +36 -0
- package/index.d.ts +99 -0
- package/index.js +262 -0
- package/lib/request.js +288 -0
- package/lib/utils.js +81 -0
- package/package.json +70 -0
- package/test/async-route-handler.js +50 -0
- package/test/base-get.js +48 -0
- package/test/base-path.js +38 -0
- package/test/base-querystring.js +48 -0
- package/test/build-url.js +69 -0
- package/test/core-with-path-in-base.js +50 -0
- package/test/custom-undici-instance.js +77 -0
- package/test/fastify-multipart-incompatibility.js +90 -0
- package/test/fixtures/fastify.cert +19 -0
- package/test/fixtures/fastify.key +27 -0
- package/test/fixtures/file.txt +1 -0
- package/test/full-get.js +45 -0
- package/test/full-https-get.js +55 -0
- package/test/full-post-extended-content-type.js +59 -0
- package/test/full-post-http2.js +49 -0
- package/test/full-post-stream-core.js +66 -0
- package/test/full-post-stream.js +64 -0
- package/test/full-post.js +56 -0
- package/test/full-querystring-rewrite-option-complex.js +48 -0
- package/test/full-querystring-rewrite-option-function.js +51 -0
- package/test/full-querystring-rewrite-option.js +48 -0
- package/test/full-querystring-rewrite-string.js +46 -0
- package/test/full-querystring-rewrite.js +46 -0
- package/test/full-querystring.js +46 -0
- package/test/full-rewrite-body-content-type.js +59 -0
- package/test/full-rewrite-body-http.js +63 -0
- package/test/full-rewrite-body-to-empty-string.js +61 -0
- package/test/full-rewrite-body-to-null.js +61 -0
- package/test/full-rewrite-body.js +61 -0
- package/test/get-upstream-http.js +70 -0
- package/test/get-upstream-undici.js +45 -0
- package/test/get-with-body.js +55 -0
- package/test/head-with-body.js +57 -0
- package/test/host-header.js +67 -0
- package/test/http-agents.js +55 -0
- package/test/http-http2.js +50 -0
- package/test/http-invalid-target.js +35 -0
- package/test/http-retry.js +109 -0
- package/test/http-timeout.js +56 -0
- package/test/http2-http2.js +57 -0
- package/test/http2-https.js +80 -0
- package/test/http2-invalid-target.js +36 -0
- package/test/http2-target-crash.js +53 -0
- package/test/http2-target-multi-crash.js +60 -0
- package/test/http2-timeout.js +92 -0
- package/test/https-agents.js +67 -0
- package/test/index.test-d.ts +136 -0
- package/test/modifyCoreObjects-false.js +48 -0
- package/test/no-body-opts-with-get.js +49 -0
- package/test/no-body-opts-with-head.js +51 -0
- package/test/no-stream-body-option.js +58 -0
- package/test/on-error.js +62 -0
- package/test/onResponse.js +48 -0
- package/test/padded-body.js +64 -0
- package/test/post-formbody.js +59 -0
- package/test/post-plain-text.js +56 -0
- package/test/post-with-custom-encoded-contenttype.js +65 -0
- package/test/retry-on-503.js +101 -0
- package/test/rewrite-headers.js +52 -0
- package/test/rewrite-request-headers.js +47 -0
- package/test/transform-body.js +61 -0
- package/test/undici-agent.js +77 -0
- package/test/undici-body.js +70 -0
- package/test/undici-options.js +73 -0
- package/test/undici-retry.js +109 -0
- package/test/undici-timeout-body-partial.js +60 -0
- package/test/undici-timeout-body.js +63 -0
- package/test/undici-timeout.js +61 -0
- package/test/undici-with-path-in-base.js +50 -0
- package/test/undici.js +50 -0
- package/test/unexpected-error.js +46 -0
- package/test/unix-http-undici-from.js +51 -0
- package/test/unix-http-undici.js +62 -0
- package/test/unix-http.js +65 -0
- package/test/unix-https-undici.js +71 -0
- 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
|
+
}
|