├── .gitattributes ├── .github ├── dependabot.yml ├── stale.yml └── workflows │ └── ci.yml ├── .gitignore ├── .npmrc ├── .taprc ├── LICENSE ├── README.md ├── eslint.config.js ├── examples └── example.js ├── index.js ├── lib ├── errors.js ├── request.js └── utils.js ├── package.json ├── test ├── async-route-handler.test.js ├── base-get.test.js ├── base-path.test.js ├── base-querystring.test.js ├── build-url.test.js ├── core-with-path-in-base.test.js ├── custom-undici-instance.test.js ├── disable-request-logging.test.js ├── fastify-multipart-incompatibility.test.js ├── fix-GHSA-v2v2-hph8-q5xp.test.js ├── fixtures │ ├── fastify.cert │ ├── fastify.key │ └── file.txt ├── full-delete-http2.test.js ├── full-get-test.test.js ├── full-https-get.test.js ├── full-post-extended-content-type.test.js ├── full-post-http2.test.js ├── full-post-stream-core.test.js ├── full-post-stream.test.js ├── full-post.test.js ├── full-querystring-rewrite-option-complex.test.js ├── full-querystring-rewrite-option-function-request.test.js ├── full-querystring-rewrite-option-function.test.js ├── full-querystring-rewrite-option.test.js ├── full-querystring-rewrite-string.test.js ├── full-querystring-rewrite.test.js ├── full-querystring.test.js ├── full-rewrite-body-content-type.test.js ├── full-rewrite-body-http.test.js ├── full-rewrite-body-to-empty-string.test.js ├── full-rewrite-body-to-null.test.js ├── full-rewrite-body.test.js ├── get-upstream-cache.test.js ├── get-upstream-http.test.js ├── get-upstream-type.test.js ├── get-upstream-undici.test.js ├── host-header.test.js ├── http-agents.test.js ├── http-global-agent.test.js ├── http-http2.test.js ├── http-invalid-target.test.js ├── http-retry.test.js ├── http-timeout.test.js ├── http2-http2.test.js ├── http2-https.test.js ├── http2-invalid-base.test.js ├── http2-invalid-target.test.js ├── http2-target-crash.test.js ├── http2-target-multi-crash.test.js ├── http2-timeout-disabled.test.js ├── http2-timeout.test.js ├── http2-unix-socket.test.js ├── https-agents.test.js ├── https-global-agent.test.js ├── method.test.js ├── modifyCoreObjects-false.test.js ├── no-body-opts-with-get.test.js ├── no-body-opts-with-head.test.js ├── no-stream-body-option.test.js ├── on-error.test.js ├── on-invalid-upstream-response.test.js ├── onResponse.test.js ├── padded-body.test.js ├── post-formbody.test.js ├── post-plain-text.test.js ├── post-with-custom-encoded-contenttype.test.js ├── post-with-octet-stream.test.js ├── retry-on-503.test.js ├── retry-with-a-custom-handler.test.js ├── rewrite-headers-type.test.js ├── rewrite-headers.test.js ├── rewrite-request-headers-type.test.js ├── rewrite-request-headers.test.js ├── transform-body.test.js ├── undici-agent.test.js ├── undici-body.test.js ├── undici-chaining.test.js ├── undici-connect-timeout.test.js ├── undici-custom-dispatcher.test.js ├── undici-global-agent.test.js ├── undici-no-destroy.test.js ├── undici-options.test.js ├── undici-proxy-agent.test.js ├── undici-retry.test.js ├── undici-timeout-body-partial.test.js ├── undici-timeout-body.test.js ├── undici-timeout.test.js ├── undici-with-path-in-base.test.js ├── undici.test.js ├── unexpected-error.test.js ├── unix-http-undici-from.test.js ├── unix-http-undici.test.js ├── unix-http.test.js ├── unix-https-undici.test.js ├── unix-https.test.js └── utils-filter-pseudo-headers.test.js └── types ├── index.d.ts └── index.test-d.ts /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set default behavior to automatically convert line endings 2 | * text=auto eol=lf 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | open-pull-requests-limit: 10 8 | 9 | - package-ecosystem: "npm" 10 | directory: "/" 11 | schedule: 12 | interval: "monthly" 13 | open-pull-requests-limit: 10 14 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 15 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - "discussion" 8 | - "feature request" 9 | - "bug" 10 | - "help wanted" 11 | - "plugin suggestion" 12 | - "good first issue" 13 | # Label to use when marking an issue as stale 14 | staleLabel: stale 15 | # Comment to post when marking an issue as stale. Set to `false` to disable 16 | markComment: > 17 | This issue has been automatically marked as stale because it has not had 18 | recent activity. It will be closed if no further activity occurs. Thank you 19 | for your contributions. 20 | # Comment to post when closing a stale issue. Set to `false` to disable 21 | closeComment: false 22 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - next 8 | - 'v*' 9 | paths-ignore: 10 | - 'docs/**' 11 | - '*.md' 12 | pull_request: 13 | paths-ignore: 14 | - 'docs/**' 15 | - '*.md' 16 | 17 | permissions: 18 | contents: read 19 | 20 | jobs: 21 | test: 22 | permissions: 23 | contents: write 24 | pull-requests: write 25 | uses: fastify/workflows/.github/workflows/plugins-ci.yml@v5 26 | with: 27 | license-check: true 28 | lint: true 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | # Vim swap files 133 | *.swp 134 | 135 | # macOS files 136 | .DS_Store 137 | 138 | # Clinic 139 | .clinic 140 | 141 | # lock files 142 | bun.lockb 143 | package-lock.json 144 | pnpm-lock.yaml 145 | yarn.lock 146 | 147 | # editor files 148 | .vscode 149 | .idea 150 | 151 | #tap files 152 | .tap/ 153 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.taprc: -------------------------------------------------------------------------------- 1 | disable-coverage: true 2 | files: 3 | - test/**/*.test.js 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Matteo Collina 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = require('neostandard')({ 4 | ignores: require('neostandard').resolveIgnoresFromGitignore(), 5 | ts: true 6 | }) 7 | -------------------------------------------------------------------------------- /examples/example.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Fastify = require('fastify') 4 | 5 | const target = Fastify({ 6 | logger: true 7 | }) 8 | 9 | target.get('/', (_request, reply) => { 10 | reply.send('hello world') 11 | }) 12 | 13 | const proxy = Fastify({ 14 | logger: true 15 | }) 16 | 17 | // proxy.register(require('fastify-reply-from'), { 18 | proxy.register(require('..'), { 19 | base: 'http://localhost:3001/' 20 | }) 21 | 22 | proxy.get('/', (_request, reply) => { 23 | reply.from('/') 24 | }) 25 | 26 | target.listen({ port: 3001 }, (err) => { 27 | if (err) { 28 | throw err 29 | } 30 | 31 | proxy.listen({ port: 3000 }, (err) => { 32 | if (err) { 33 | throw err 34 | } 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /lib/errors.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const createError = require('@fastify/error') 4 | 5 | module.exports.TimeoutError = createError('FST_REPLY_FROM_TIMEOUT', 'Timeout', 504) 6 | module.exports.HttpRequestTimeoutError = createError('FST_REPLY_FROM_HTTP_REQUEST_TIMEOUT', 'HTTP request timed out', 504, module.exports.TimeoutError) 7 | module.exports.Http2RequestTimeoutError = createError('FST_REPLY_FROM_HTTP2_REQUEST_TIMEOUT', 'HTTP/2 request timed out', 504, module.exports.TimeoutError) 8 | module.exports.Http2SessionTimeoutError = createError('FST_REPLY_FROM_HTTP2_SESSION_TIMEOUT', 'HTTP/2 session timed out', 504, module.exports.TimeoutError) 9 | module.exports.ServiceUnavailableError = createError('FST_REPLY_FROM_SERVICE_UNAVAILABLE', 'Service Unavailable', 503) 10 | module.exports.GatewayTimeoutError = createError('FST_REPLY_FROM_GATEWAY_TIMEOUT', 'Gateway Timeout', 504) 11 | module.exports.ConnectionResetError = createError('ECONNRESET', 'Connection Reset', 500) 12 | module.exports.ConnectTimeoutError = createError('UND_ERR_CONNECT_TIMEOUT', 'Connect Timeout Error', 500) 13 | module.exports.UndiciSocketError = createError('UND_ERR_SOCKET', 'Undici Socket Error', 500) 14 | module.exports.InternalServerError = createError('FST_REPLY_FROM_INTERNAL_SERVER_ERROR', '%s', 500) 15 | module.exports.BadGatewayError = createError('FST_REPLY_FROM_BAD_GATEWAY', 'Bad Gateway', 502) 16 | -------------------------------------------------------------------------------- /lib/request.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const http = require('node:http') 3 | const https = require('node:https') 4 | const querystring = require('node:querystring') 5 | const eos = require('end-of-stream') 6 | const { pipeline } = require('node:stream') 7 | const undici = require('undici') 8 | const { stripHttp1ConnectionHeaders } = require('./utils') 9 | const http2 = require('node:http2') 10 | 11 | const { 12 | TimeoutError, 13 | Http2RequestTimeoutError, 14 | Http2SessionTimeoutError, 15 | HttpRequestTimeoutError 16 | } = require('./errors') 17 | 18 | function shouldUseUndici (opts) { 19 | if (opts.undici === false || opts.http || opts.http2) { 20 | return false 21 | } 22 | return true 23 | } 24 | 25 | function isRequestable (obj) { 26 | return obj !== null && 27 | typeof obj === 'object' && 28 | typeof obj.request === 'function' 29 | } 30 | 31 | function isUndiciInstance (obj) { 32 | return obj instanceof undici.Pool || 33 | obj instanceof undici.Client || 34 | obj instanceof undici.Dispatcher || 35 | isRequestable(obj) 36 | } 37 | 38 | function buildRequest (opts) { 39 | const isHttp2 = !!opts.http2 40 | const hasUndiciOptions = shouldUseUndici(opts) 41 | const requests = { 42 | 'http:': http, 43 | 'https:': https, 44 | 'unix+http:': { base: http, request: unixRequest }, 45 | 'unix+https:': { base: https, request: unixRequest } 46 | } 47 | const http2Opts = getHttp2Opts(opts) 48 | const httpOpts = getHttpOpts(opts) 49 | const baseUrl = opts.base && new URL(opts.base).origin 50 | const undiciOpts = opts.undici || {} 51 | const globalAgent = opts.globalAgent 52 | const destroyAgent = opts.destroyAgent 53 | let http2Client 54 | let undiciAgent 55 | let undiciInstance 56 | let agents 57 | 58 | if (isHttp2) { 59 | if (!opts.base) return new Error('Option base is required when http2 is true') 60 | if (opts.base.startsWith('unix+')) { 61 | return new Error('Unix socket destination is not supported when http2 is true') 62 | } 63 | } else if (!globalAgent) { 64 | agents = httpOpts.agents || { 65 | 'http:': new http.Agent(httpOpts.agentOptions), 66 | 'https:': new https.Agent(httpOpts.agentOptions) 67 | } 68 | } else { 69 | agents = { 70 | 'http:': http.globalAgent, 71 | 'https:': https.globalAgent 72 | } 73 | } 74 | 75 | if (isHttp2) { 76 | return { request: handleHttp2Req, close, retryOnError: 'ECONNRESET' } 77 | } else if (hasUndiciOptions) { 78 | if (opts.base?.startsWith('unix+')) { 79 | const undiciOpts = getUndiciOptions(opts.undici) 80 | undiciOpts.socketPath = decodeURIComponent(new URL(opts.base).host) 81 | const protocol = opts.base.startsWith('unix+https') ? 'https' : 'http' 82 | undiciInstance = new undici.Pool(protocol + '://localhost', undiciOpts) 83 | } else if (isUndiciInstance(opts.undici)) { 84 | undiciInstance = opts.undici 85 | } else if (!globalAgent) { 86 | if (undiciOpts.proxy) { 87 | undiciAgent = new undici.ProxyAgent(getUndiciProxyOptions(opts.undici)) 88 | } else { 89 | undiciAgent = new undici.Agent(getUndiciOptions(opts.undici)) 90 | } 91 | } else { 92 | undiciAgent = undici.getGlobalDispatcher() 93 | } 94 | return { request: handleUndici, close, retryOnError: 'UND_ERR_SOCKET' } 95 | } else { 96 | return { request: handleHttp1Req, close, retryOnError: 'ECONNRESET' } 97 | } 98 | 99 | function close () { 100 | if (globalAgent || destroyAgent === false) { 101 | return 102 | } 103 | 104 | if (hasUndiciOptions) { 105 | undiciAgent?.destroy() 106 | undiciInstance?.destroy() 107 | } else if (!isHttp2) { 108 | agents['http:'].destroy() 109 | agents['https:'].destroy() 110 | } else if (http2Client) { 111 | http2Client.destroy() 112 | } 113 | } 114 | 115 | function handleHttp1Req (opts, done) { 116 | const req = requests[opts.url.protocol].request({ 117 | method: opts.method, 118 | port: opts.url.port, 119 | path: opts.url.pathname + opts.qs, 120 | hostname: opts.url.hostname, 121 | headers: opts.headers, 122 | agent: agents[opts.url.protocol.replace(/^unix:/, '')], 123 | ...httpOpts.requestOptions, 124 | timeout: opts.timeout ?? httpOpts.requestOptions.timeout 125 | }) 126 | req.on('error', done) 127 | req.on('response', res => { 128 | // remove timeout for sse connections 129 | if (res.headers['content-type'] === 'text/event-stream') { 130 | req.setTimeout(0) 131 | } 132 | done(null, { statusCode: res.statusCode, headers: res.headers, stream: res }) 133 | }) 134 | req.once('timeout', () => { 135 | const err = new HttpRequestTimeoutError() 136 | req.abort() 137 | done(err) 138 | }) 139 | 140 | end(req, opts.body, done) 141 | } 142 | 143 | function handleUndici (opts, done) { 144 | const req = { 145 | origin: baseUrl || opts.url.origin, 146 | path: opts.url.pathname + opts.qs, 147 | method: opts.method, 148 | headers: Object.assign({}, opts.headers), 149 | body: opts.body, 150 | headersTimeout: opts.timeout ?? undiciOpts.headersTimeout, 151 | bodyTimeout: opts.timeout ?? undiciOpts.bodyTimeout 152 | } 153 | 154 | let pool 155 | 156 | if (undiciInstance) { 157 | pool = undiciInstance 158 | } else if (!baseUrl && opts.url.protocol.startsWith('unix')) { 159 | done(new Error('unix socket not supported with undici yet')) 160 | return 161 | } else { 162 | pool = undiciAgent 163 | } 164 | 165 | // remove forbidden headers 166 | req.headers.connection = undefined 167 | req.headers['transfer-encoding'] = undefined 168 | 169 | pool.request(req, function (err, res) { 170 | if (err) { 171 | done(err) 172 | return 173 | } 174 | 175 | // using delete, otherwise it will render as an empty string 176 | delete res.headers['transfer-encoding'] 177 | 178 | done(null, { statusCode: res.statusCode, headers: res.headers, stream: res.body }) 179 | }) 180 | } 181 | 182 | function handleHttp2Req (opts, done) { 183 | let cancelRequest 184 | let sessionTimedOut = false 185 | 186 | if (!http2Client || http2Client.destroyed) { 187 | http2Client = http2.connect(baseUrl, http2Opts.sessionOptions) 188 | http2Client.once('error', done) 189 | // we might enqueue a large number of requests in this connection 190 | // before it's connected 191 | http2Client.setMaxListeners(0) 192 | http2Client.setTimeout(http2Opts.sessionTimeout, function () { 193 | if (cancelRequest) { 194 | cancelRequest() 195 | cancelRequest = undefined 196 | sessionTimedOut = true 197 | } 198 | http2Client.destroy() 199 | }) 200 | http2Client.once('connect', () => { 201 | // reset the max listener to 10 on connect 202 | http2Client.setMaxListeners(10) 203 | http2Client.removeListener('error', done) 204 | }) 205 | } 206 | const req = http2Client.request({ 207 | ':method': opts.method, 208 | ':path': opts.url.pathname + opts.qs, 209 | ...stripHttp1ConnectionHeaders(opts.headers) 210 | }, http2Opts.requestOptions) 211 | const isGet = opts.method === 'GET' || opts.method === 'get' 212 | const isDelete = opts.method === 'DELETE' || opts.method === 'delete' 213 | if (!isGet && !isDelete) { 214 | end(req, opts.body, done) 215 | } 216 | req.setTimeout(opts.timeout ?? http2Opts.requestTimeout, () => { 217 | const err = new Http2RequestTimeoutError() 218 | req.close(http2.constants.NGHTTP2_CANCEL) 219 | done(err) 220 | }) 221 | req.once('close', () => { 222 | if (sessionTimedOut) { 223 | const err = new Http2SessionTimeoutError() 224 | done(err) 225 | } 226 | }) 227 | cancelRequest = eos(req, err => { 228 | if (err) done(err) 229 | }) 230 | req.on('response', headers => { 231 | // remove timeout for sse connections 232 | if (headers['content-type'] === 'text/event-stream') { 233 | req.setTimeout(0) 234 | http2Client.setTimeout(0) 235 | } 236 | 237 | const statusCode = headers[':status'] 238 | done(null, { statusCode, headers, stream: req }) 239 | }) 240 | } 241 | } 242 | 243 | module.exports = buildRequest 244 | module.exports.TimeoutError = TimeoutError 245 | 246 | function unixRequest (opts) { 247 | delete opts.port 248 | opts.socketPath = querystring.unescape(opts.hostname) 249 | delete opts.hostname 250 | return this.base.request(opts) 251 | } 252 | 253 | function end (req, body, cb) { 254 | if (!body || typeof body === 'string' || body instanceof Uint8Array) { 255 | req.end(body) 256 | } else if (body.pipe) { 257 | pipeline(body, req, err => { 258 | if (err) cb(err) 259 | }) 260 | } else { 261 | cb(new Error(`type unsupported for body: ${body.constructor}`)) 262 | } 263 | } 264 | 265 | function getHttp2Opts (opts) { 266 | if (!opts.http2) { 267 | return {} 268 | } 269 | 270 | let http2Opts = opts.http2 271 | if (typeof http2Opts === 'boolean') { 272 | http2Opts = {} 273 | } 274 | http2Opts.sessionOptions = http2Opts.sessionOptions || {} 275 | 276 | if (http2Opts.sessionTimeout === undefined) { 277 | http2Opts.sessionTimeout = opts.sessionTimeout || 60000 278 | } 279 | if (http2Opts.requestTimeout === undefined) { 280 | http2Opts.requestTimeout = 10000 281 | } 282 | http2Opts.sessionOptions.rejectUnauthorized = http2Opts.sessionOptions.rejectUnauthorized || false 283 | 284 | return http2Opts 285 | } 286 | 287 | function getHttpOpts (opts) { 288 | const httpOpts = typeof opts.http === 'object' ? opts.http : {} 289 | httpOpts.requestOptions = httpOpts.requestOptions || {} 290 | 291 | if (!httpOpts.requestOptions.timeout) { 292 | httpOpts.requestOptions.timeout = 10000 293 | } 294 | 295 | httpOpts.requestOptions.rejectUnauthorized = httpOpts.requestOptions.rejectUnauthorized || false 296 | 297 | httpOpts.agentOptions = getAgentOptions(opts) 298 | 299 | return httpOpts 300 | } 301 | 302 | function getAgentOptions (opts) { 303 | return { 304 | keepAlive: true, 305 | keepAliveMsecs: 60 * 1000, // 1 minute 306 | maxSockets: 2048, 307 | maxFreeSockets: 2048, 308 | ...(opts.http?.agentOptions) 309 | } 310 | } 311 | 312 | function getUndiciProxyOptions ({ proxy, ...opts }) { 313 | if (typeof proxy === 'string' || proxy instanceof URL) { 314 | return getUndiciOptions({ uri: proxy, ...opts }) 315 | } 316 | return getUndiciOptions({ ...proxy, ...opts }) 317 | } 318 | 319 | function getUndiciOptions (opts = {}) { 320 | const res = { 321 | pipelining: 1, 322 | connections: 128, 323 | tls: {}, 324 | ...(opts) 325 | } 326 | 327 | res.tls.rejectUnauthorized = res.tls.rejectUnauthorized || false 328 | 329 | return res 330 | } 331 | 332 | module.exports.getUndiciOptions = getUndiciOptions 333 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | function filterPseudoHeaders (headers) { 4 | const dest = {} 5 | const headersKeys = Object.keys(headers) 6 | let header 7 | let i 8 | for (i = 0; i < headersKeys.length; i++) { 9 | header = headersKeys[i] 10 | if (header.charCodeAt(0) !== 58) { // fast path for indexOf(':') === 0 11 | dest[header.toLowerCase()] = headers[header] 12 | } 13 | } 14 | return dest 15 | } 16 | 17 | function copyHeaders (headers, reply) { 18 | const headersKeys = Object.keys(headers) 19 | 20 | let header 21 | let i 22 | 23 | for (i = 0; i < headersKeys.length; i++) { 24 | header = headersKeys[i] 25 | if (header.charCodeAt(0) !== 58) { // fast path for indexOf(':') === 0 26 | reply.header(header, headers[header]) 27 | } 28 | } 29 | } 30 | 31 | function stripHttp1ConnectionHeaders (headers) { 32 | const headersKeys = Object.keys(headers) 33 | const dest = {} 34 | 35 | let header 36 | let i 37 | 38 | for (i = 0; i < headersKeys.length; i++) { 39 | header = headersKeys[i].toLowerCase() 40 | 41 | switch (header) { 42 | case 'connection': 43 | case 'upgrade': 44 | case 'http2-settings': 45 | case 'te': 46 | case 'transfer-encoding': 47 | case 'proxy-connection': 48 | case 'keep-alive': 49 | case 'host': 50 | break 51 | default: 52 | dest[header] = headers[header] 53 | break 54 | } 55 | } 56 | return dest 57 | } 58 | 59 | // issue ref: https://github.com/fastify/fast-proxy/issues/42 60 | function buildURL (source, reqBase) { 61 | let baseOrigin = reqBase ? new URL(reqBase).href : undefined 62 | 63 | // To make sure we don't accidentally override the base path 64 | if (baseOrigin && source.length > 1 && source[0] === '/' && source[1] === '/') { 65 | source = '.' + source 66 | } 67 | 68 | const dest = new URL(source, reqBase) 69 | 70 | // if base is specified, source url should not override it 71 | if (baseOrigin) { 72 | if (!baseOrigin.endsWith('/') && dest.href.length > baseOrigin.length) { 73 | baseOrigin = baseOrigin + '/' 74 | } 75 | 76 | if (!dest.href.startsWith(baseOrigin)) { 77 | throw new Error('source must be a relative path string') 78 | } 79 | } 80 | 81 | return dest 82 | } 83 | 84 | module.exports = { 85 | copyHeaders, 86 | stripHttp1ConnectionHeaders, 87 | filterPseudoHeaders, 88 | buildURL 89 | } 90 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@fastify/reply-from", 3 | "version": "12.1.0", 4 | "description": "forward your HTTP request to another server, for fastify", 5 | "main": "index.js", 6 | "type": "commonjs", 7 | "types": "types/index.d.ts", 8 | "scripts": { 9 | "lint": "eslint", 10 | "lint:fix": "eslint --fix", 11 | "test": "npm run test:unit && npm run test:typescript", 12 | "test:unit": "c8 node --test", 13 | "test:typescript": "tsd" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/fastify/fastify-reply-from.git" 18 | }, 19 | "keywords": [ 20 | "fastify", 21 | "http", 22 | "forward", 23 | "proxy" 24 | ], 25 | "author": "Matteo Collina ", 26 | "contributors": [ 27 | { 28 | "name": "James Sumners", 29 | "url": "https://james.sumners.info" 30 | }, 31 | { 32 | "name": "Manuel Spigolon", 33 | "email": "behemoth89@gmail.com" 34 | }, 35 | { 36 | "name": "Aras Abbasi", 37 | "email": "aras.abbasi@gmail.com" 38 | }, 39 | { 40 | "name": "Frazer Smith", 41 | "email": "frazer.dev@icloud.com", 42 | "url": "https://github.com/fdawgs" 43 | } 44 | ], 45 | "license": "MIT", 46 | "bugs": { 47 | "url": "https://github.com/fastify/fastify-reply-from/issues" 48 | }, 49 | "homepage": "https://github.com/fastify/fastify-reply-from#readme", 50 | "funding": [ 51 | { 52 | "type": "github", 53 | "url": "https://github.com/sponsors/fastify" 54 | }, 55 | { 56 | "type": "opencollective", 57 | "url": "https://opencollective.com/fastify" 58 | } 59 | ], 60 | "devDependencies": { 61 | "@fastify/formbody": "^8.0.0", 62 | "@fastify/multipart": "^9.0.0", 63 | "@fastify/pre-commit": "^2.1.0", 64 | "@sinonjs/fake-timers": "^14.0.0", 65 | "@types/node": "^22.0.0", 66 | "@types/tap": "^18.0.0", 67 | "c8": "^10.1.3", 68 | "eslint": "^9.17.0", 69 | "fastify": "^5.0.0", 70 | "form-data": "^4.0.0", 71 | "h2url": "^0.2.0", 72 | "neostandard": "^0.12.0", 73 | "nock": "^14.0.0", 74 | "proxy": "^2.1.1", 75 | "proxyquire": "^2.1.3", 76 | "split2": "^4.2.0", 77 | "tsd": "^0.32.0" 78 | }, 79 | "dependencies": { 80 | "@fastify/error": "^4.0.0", 81 | "end-of-stream": "^1.4.4", 82 | "fast-content-type-parse": "^3.0.0", 83 | "fast-querystring": "^1.1.2", 84 | "fastify-plugin": "^5.0.1", 85 | "toad-cache": "^3.7.0", 86 | "undici": "^7.0.0" 87 | }, 88 | "pre-commit": [ 89 | "lint", 90 | "test" 91 | ], 92 | "publishConfig": { 93 | "access": "public" 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /test/async-route-handler.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const t = require('node:test') 4 | const Fastify = require('fastify') 5 | const From = require('..') 6 | const http = require('node:http') 7 | const { request } = require('undici') 8 | 9 | const instance = Fastify() 10 | 11 | t.test('async route handler', async (t) => { 12 | t.plan(8) 13 | t.after(() => instance.close()) 14 | 15 | const target = http.createServer((req, res) => { 16 | t.assert.ok('request proxied') 17 | t.assert.strictEqual(req.method, 'GET') 18 | t.assert.strictEqual(req.url, '/') 19 | res.statusCode = 205 20 | res.setHeader('Content-Type', 'text/plain') 21 | res.setHeader('x-my-header', 'hello!') 22 | res.end('hello world') 23 | }) 24 | 25 | instance.get('/', async (_request, reply) => { 26 | const p = reply.from() 27 | t.assert.strictEqual(p, reply) 28 | return p 29 | }) 30 | 31 | t.after(() => target.close()) 32 | 33 | await new Promise(resolve => target.listen({ port: 0 }, resolve)) 34 | 35 | instance.register(From, { 36 | base: `http://localhost:${target.address().port}` 37 | }) 38 | 39 | await new Promise(resolve => instance.listen({ port: 0 }, resolve)) 40 | 41 | const result = await request(`http://localhost:${instance.server.address().port}`) 42 | 43 | t.assert.strictEqual(result.headers['content-type'], 'text/plain') 44 | t.assert.strictEqual(result.headers['x-my-header'], 'hello!') 45 | t.assert.strictEqual(result.statusCode, 205) 46 | t.assert.strictEqual(await result.body.text(), 'hello world') 47 | }) 48 | -------------------------------------------------------------------------------- /test/base-get.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const t = require('node:test') 4 | const Fastify = require('fastify') 5 | const { request } = require('undici') 6 | const From = require('..') 7 | const http = require('node:http') 8 | 9 | const instance = Fastify() 10 | 11 | t.test('base get', async (t) => { 12 | t.plan(7) 13 | t.after(() => instance.close()) 14 | 15 | const target = http.createServer((req, res) => { 16 | t.assert.ok('request proxied') 17 | t.assert.strictEqual(req.method, 'GET') 18 | t.assert.strictEqual(req.url, '/') 19 | res.statusCode = 205 20 | res.setHeader('Content-Type', 'text/plain') 21 | res.setHeader('x-my-header', 'hello!') 22 | res.end('hello world') 23 | }) 24 | 25 | instance.get('/', (_request, reply) => { 26 | reply.from() 27 | }) 28 | 29 | t.after(() => target.close()) 30 | 31 | await new Promise((resolve) => target.listen({ port: 0 }, resolve)) 32 | 33 | instance.register(From, { 34 | base: `http://localhost:${target.address().port}` 35 | }) 36 | 37 | await new Promise((resolve) => instance.listen({ port: 0 }, resolve)) 38 | 39 | const result = await request(`http://localhost:${instance.server.address().port}`) 40 | 41 | t.assert.strictEqual(result.headers['content-type'], 'text/plain') 42 | t.assert.strictEqual(result.headers['x-my-header'], 'hello!') 43 | t.assert.strictEqual(result.statusCode, 205) 44 | t.assert.strictEqual(await result.body.text(), 'hello world') 45 | }) 46 | -------------------------------------------------------------------------------- /test/base-path.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const t = require('node:test') 4 | const Fastify = require('fastify') 5 | const { request, Agent } = require('undici') 6 | const From = require('..') 7 | const nock = require('nock') 8 | 9 | t.test('base path', async (t) => { 10 | const instance = Fastify() 11 | 12 | nock('http://httpbin.org') 13 | .get('/ip') 14 | .reply(200, function () { 15 | t.assert.strictEqual(this.req.headers.host, 'httpbin.org') 16 | return { origin: '127.0.0.1' } 17 | }) 18 | 19 | t.plan(4) 20 | t.after(() => instance.close()) 21 | 22 | instance.get('/', (_request, reply) => { 23 | reply.from('http://httpbin.org/ip') 24 | }) 25 | 26 | instance.register(From, { 27 | undici: false 28 | }) 29 | 30 | await new Promise(resolve => instance.listen({ port: 0 }, resolve)) 31 | 32 | const result = await request(`http://localhost:${instance.server.address().port}`, { 33 | dispatcher: new Agent({ 34 | pipelining: 0 35 | }) 36 | }) 37 | 38 | t.assert.strictEqual(result.statusCode, 200) 39 | t.assert.strictEqual(result.headers['content-type'], 'application/json') 40 | t.assert.strictEqual(typeof (await result.body.json()).origin, 'string') 41 | }) 42 | -------------------------------------------------------------------------------- /test/base-querystring.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const t = require('node:test') 4 | const Fastify = require('fastify') 5 | const { request } = require('undici') 6 | const From = require('..') 7 | const http = require('node:http') 8 | 9 | const instance = Fastify() 10 | 11 | t.test('base querystring', async (t) => { 12 | t.plan(7) 13 | t.after(() => instance.close()) 14 | 15 | const target = http.createServer((req, res) => { 16 | t.assert.ok('request proxied') 17 | t.assert.strictEqual(req.method, 'GET') 18 | t.assert.strictEqual(req.url, '/hello?a=b') 19 | res.statusCode = 205 20 | res.setHeader('Content-Type', 'text/plain') 21 | res.setHeader('x-my-header', 'hello!') 22 | res.end('hello world') 23 | }) 24 | 25 | instance.get('/hello', (_request, reply) => { 26 | reply.from() 27 | }) 28 | 29 | t.after(() => target.close()) 30 | 31 | await new Promise(resolve => target.listen({ port: 0 }, resolve)) 32 | 33 | instance.register(From, { 34 | base: `http://localhost:${target.address().port}` 35 | }) 36 | 37 | await new Promise(resolve => instance.listen({ port: 0 }, resolve)) 38 | 39 | const result = await request(`http://localhost:${instance.server.address().port}/hello?a=b`) 40 | 41 | t.assert.strictEqual(result.headers['content-type'], 'text/plain') 42 | t.assert.strictEqual(result.headers['x-my-header'], 'hello!') 43 | t.assert.strictEqual(result.statusCode, 205) 44 | t.assert.strictEqual(await result.body.text(), 'hello world') 45 | }) 46 | -------------------------------------------------------------------------------- /test/build-url.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const { buildURL } = require('../lib/utils') 5 | 6 | test('should produce valid URL', (t) => { 7 | t.plan(1) 8 | const url = buildURL('/hi', 'http://localhost') 9 | t.assert.strictEqual(url.href, 'http://localhost/hi') 10 | }) 11 | 12 | test('should produce valid URL', (t) => { 13 | t.plan(1) 14 | const url = buildURL('http://localhost/hi', 'http://localhost') 15 | t.assert.strictEqual(url.href, 'http://localhost/hi') 16 | }) 17 | 18 | test('should return same source when base is not specified', (t) => { 19 | t.plan(1) 20 | const url = buildURL('http://localhost/hi') 21 | t.assert.strictEqual(url.href, 'http://localhost/hi') 22 | }) 23 | 24 | test('should handle lack of trailing slash in base', (t) => { 25 | t.plan(3) 26 | let url = buildURL('hi', 'http://localhost/hi') 27 | t.assert.strictEqual(url.href, 'http://localhost/hi') 28 | 29 | url = buildURL('hi/', 'http://localhost/hi') 30 | t.assert.strictEqual(url.href, 'http://localhost/hi/') 31 | 32 | url = buildURL('hi/more', 'http://localhost/hi') 33 | t.assert.strictEqual(url.href, 'http://localhost/hi/more') 34 | }) 35 | 36 | test('should handle default port in base', (t) => { 37 | t.plan(2) 38 | let url = buildURL('/hi', 'http://localhost:80/hi') 39 | t.assert.strictEqual(url.href, 'http://localhost/hi') 40 | 41 | url = buildURL('/hi', 'https://localhost:443/hi') 42 | t.assert.strictEqual(url.href, 'https://localhost/hi') 43 | }) 44 | 45 | test('should append instead of override base', (t) => { 46 | t.plan(2) 47 | let url = buildURL('//10.0.0.10/hi', 'http://localhost') 48 | t.assert.strictEqual(url.href, 'http://localhost//10.0.0.10/hi') 49 | 50 | url = buildURL('//httpbin.org/hi', 'http://localhost') 51 | t.assert.strictEqual(url.href, 'http://localhost//httpbin.org/hi') 52 | }) 53 | 54 | const errorInputs = [ 55 | { source: 'http://10.0.0.10/hi', base: 'http://localhost' }, 56 | { source: 'https://10.0.0.10/hi', base: 'http://localhost' }, 57 | { source: 'blah://10.0.0.10/hi', base: 'http://localhost' }, 58 | { source: 'urn:foo:bar', base: 'http://localhost' }, 59 | { source: 'http://localhost/private', base: 'http://localhost/exposed/' }, 60 | { source: 'http://localhost/exposed-extra', base: 'http://localhost/exposed' }, 61 | { source: '/private', base: 'http://localhost/exposed/' }, 62 | { source: '/exposed-extra', base: 'http://localhost/exposed' }, 63 | { source: '../private', base: 'http://localhost/exposed/' }, 64 | { source: 'exposed-extra', base: 'http://localhost/exposed' } 65 | ] 66 | 67 | test('should throw when trying to override base', async (t) => { 68 | t.plan(errorInputs.length) 69 | 70 | const promises = errorInputs.map(({ source, base }) => { 71 | return t.test(source, (t) => { 72 | t.plan(1) 73 | t.assert.throws(() => buildURL(source, base)) 74 | }) 75 | }) 76 | 77 | await Promise.all(promises) 78 | }) 79 | -------------------------------------------------------------------------------- /test/core-with-path-in-base.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const t = require('node:test') 4 | const Fastify = require('fastify') 5 | const { request, Agent } = require('undici') 6 | const From = require('..') 7 | const http = require('node:http') 8 | 9 | const instance = Fastify() 10 | 11 | t.test('core with path in base', async (t) => { 12 | t.plan(8) 13 | t.after(() => instance.close()) 14 | 15 | const target = http.createServer((req, res) => { 16 | t.assert.ok('request proxied') 17 | t.assert.strictEqual(req.method, 'GET') 18 | t.assert.strictEqual(req.url, '/hello') 19 | t.assert.strictEqual(req.headers.connection, 'close') 20 | res.statusCode = 205 21 | res.setHeader('Content-Type', 'text/plain') 22 | res.setHeader('x-my-header', 'hello!') 23 | res.end('hello world') 24 | }) 25 | 26 | instance.get('/', (_request, reply) => { 27 | reply.from('/hello') 28 | }) 29 | 30 | t.after(() => target.close()) 31 | 32 | await new Promise(resolve => target.listen({ port: 0 }, resolve)) 33 | 34 | instance.register(From, { 35 | base: `http://localhost:${target.address().port}/hello`, 36 | http: true 37 | }) 38 | 39 | await new Promise(resolve => instance.listen({ port: 0 }, resolve)) 40 | 41 | const result = await request(`http://localhost:${instance.server.address().port}`, { 42 | dispatcher: new Agent({ 43 | pipelining: 0 44 | }) 45 | }) 46 | t.assert.strictEqual(result.headers['content-type'], 'text/plain') 47 | t.assert.strictEqual(result.headers['x-my-header'], 'hello!') 48 | t.assert.strictEqual(result.statusCode, 205) 49 | t.assert.strictEqual(await result.body.text(), 'hello world') 50 | }) 51 | -------------------------------------------------------------------------------- /test/custom-undici-instance.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const t = require('node:test') 4 | const assert = require('node:assert') 5 | const Fastify = require('fastify') 6 | const { Pool, request, Client } = require('undici') 7 | const http = require('node:http') 8 | const From = require('..') 9 | 10 | const target = http.createServer((req, res) => { 11 | assert.ok('request proxied') 12 | assert.strictEqual(req.method, 'GET') 13 | assert.strictEqual(req.url, '/') 14 | assert.strictEqual(req.headers.connection, 'keep-alive') 15 | res.statusCode = 205 16 | res.setHeader('Content-Type', 'text/plain') 17 | res.setHeader('x-my-header', 'hello!') 18 | res.end('hello world') 19 | }) 20 | 21 | t.test('use a custom instance of \'undici\'', async t => { 22 | t.after(() => target.close()) 23 | 24 | await new Promise((resolve, reject) => target.listen({ port: 0 }, err => err ? reject(err) : resolve())) 25 | 26 | await t.test('custom Pool', async t => { 27 | const instance = Fastify() 28 | t.after(() => instance.close()) 29 | instance.register(From, { 30 | base: `http://localhost:${target.address().port}`, 31 | undici: new Pool(`http://localhost:${target.address().port}`) 32 | }) 33 | 34 | instance.get('/', (_request, reply) => { 35 | reply.from() 36 | }) 37 | 38 | await new Promise(resolve => instance.listen({ port: 0 }, resolve)) 39 | 40 | const result = await request(`http://localhost:${instance.server.address().port}`) 41 | 42 | t.assert.strictEqual(result.headers['content-type'], 'text/plain') 43 | t.assert.strictEqual(result.headers['x-my-header'], 'hello!') 44 | t.assert.strictEqual(result.statusCode, 205) 45 | t.assert.strictEqual(await result.body.text(), 'hello world') 46 | }) 47 | 48 | await t.test('custom Client', async t => { 49 | const instance = Fastify() 50 | t.after(() => instance.close()) 51 | instance.register(From, { 52 | base: `http://localhost:${target.address().port}`, 53 | undici: new Client(`http://localhost:${target.address().port}`) 54 | }) 55 | 56 | instance.get('/', (_request, reply) => { 57 | reply.from() 58 | }) 59 | 60 | await new Promise(resolve => instance.listen({ port: 0 }, resolve)) 61 | 62 | const result = await request(`http://localhost:${instance.server.address().port}`) 63 | 64 | t.assert.strictEqual(result.headers['content-type'], 'text/plain') 65 | t.assert.strictEqual(result.headers['x-my-header'], 'hello!') 66 | t.assert.strictEqual(result.statusCode, 205) 67 | t.assert.strictEqual(await result.body.text(), 'hello world') 68 | }) 69 | }) 70 | -------------------------------------------------------------------------------- /test/disable-request-logging.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const t = require('node:test') 4 | const assert = require('node:assert') 5 | const Fastify = require('fastify') 6 | const { request } = require('undici') 7 | const From = require('..') 8 | const http = require('node:http') 9 | const split = require('split2') 10 | 11 | const target = http.createServer((req, res) => { 12 | assert.ok('request proxied') 13 | assert.strictEqual(req.method, 'GET') 14 | assert.strictEqual(req.url, '/') 15 | assert.strictEqual(req.headers.connection, 'keep-alive') 16 | res.statusCode = 205 17 | res.setHeader('Content-Type', 'text/plain') 18 | res.setHeader('x-my-header', 'hello!') 19 | res.end('hello world') 20 | }) 21 | 22 | t.test('use a custom instance of \'undici\'', async t => { 23 | t.plan(3) 24 | t.after(() => target.close()) 25 | 26 | await new Promise((resolve, reject) => target.listen({ port: 0 }, err => err ? reject(err) : resolve())) 27 | 28 | await t.test('disableRequestLogging is set to true', async t => { 29 | const logStream = split(JSON.parse) 30 | const instance = Fastify({ 31 | logger: { 32 | level: 'info', 33 | stream: logStream 34 | } 35 | }) 36 | t.after(() => instance.close()) 37 | instance.register(From, { 38 | base: `http://localhost:${target.address().port}`, 39 | disableRequestLogging: true 40 | }) 41 | 42 | instance.get('/', (_request, reply) => { 43 | reply.from() 44 | }) 45 | 46 | logStream.on('data', (log) => { 47 | if ( 48 | log.level === 30 && 49 | ( 50 | !log.msg.match('response received') || 51 | !log.msg.match('fetching from remote server') 52 | ) 53 | ) { 54 | t.assert.ok('request log message does not logged') 55 | } 56 | }) 57 | 58 | await new Promise(resolve => instance.listen({ port: 0 }, resolve)) 59 | 60 | const result = await request(`http://localhost:${instance.server.address().port}`) 61 | t.assert.strictEqual(result.headers['content-type'], 'text/plain') 62 | t.assert.strictEqual(result.headers['x-my-header'], 'hello!') 63 | t.assert.strictEqual(result.statusCode, 205) 64 | t.assert.strictEqual(await result.body.text(), 'hello world') 65 | }) 66 | 67 | await t.test('disableRequestLogging is set to false', async t => { 68 | const logStream = split(JSON.parse) 69 | const instance = Fastify({ 70 | logger: { 71 | level: 'info', 72 | stream: logStream 73 | } 74 | }) 75 | t.after(() => instance.close()) 76 | instance.register(From, { 77 | base: `http://localhost:${target.address().port}`, 78 | disableRequestLogging: false 79 | }) 80 | 81 | instance.get('/', (_request, reply) => { 82 | reply.from() 83 | }) 84 | 85 | logStream.on('data', (log) => { 86 | if ( 87 | log.level === 30 && 88 | ( 89 | log.msg.match('response received') || 90 | log.msg.match('fetching from remote server') 91 | ) 92 | ) { 93 | t.assert.ok('request log message does not logged') 94 | } 95 | }) 96 | 97 | await new Promise(resolve => instance.listen({ port: 0 }, resolve)) 98 | 99 | const result = await request(`http://localhost:${instance.server.address().port}`) 100 | t.assert.strictEqual(result.headers['content-type'], 'text/plain') 101 | t.assert.strictEqual(result.headers['x-my-header'], 'hello!') 102 | t.assert.strictEqual(result.statusCode, 205) 103 | t.assert.strictEqual(await result.body.text(), 'hello world') 104 | }) 105 | 106 | await t.test('disableRequestLogging is not defined', async t => { 107 | const logStream = split(JSON.parse) 108 | const instance = Fastify({ 109 | logger: { 110 | level: 'info', 111 | stream: logStream 112 | } 113 | }) 114 | t.after(() => instance.close()) 115 | instance.register(From, { 116 | base: `http://localhost:${target.address().port}` 117 | }) 118 | 119 | instance.get('/', (_request, reply) => { 120 | reply.from() 121 | }) 122 | 123 | logStream.on('data', (log) => { 124 | if ( 125 | log.level === 30 && 126 | ( 127 | log.msg.match('response received') || 128 | log.msg.match('fetching from remote server') 129 | ) 130 | ) { 131 | t.assert.ok('request log message does not logged') 132 | } 133 | }) 134 | 135 | await new Promise(resolve => instance.listen({ port: 0 }, resolve)) 136 | 137 | const result = await request(`http://localhost:${instance.server.address().port}`) 138 | t.assert.strictEqual(result.headers['content-type'], 'text/plain') 139 | t.assert.strictEqual(result.headers['x-my-header'], 'hello!') 140 | t.assert.strictEqual(result.statusCode, 205) 141 | t.assert.strictEqual(await result.body.text(), 'hello world') 142 | }) 143 | }) 144 | -------------------------------------------------------------------------------- /test/fastify-multipart-incompatibility.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fs = require('node:fs') 4 | const path = require('node:path') 5 | const t = require('node:test') 6 | const Fastify = require('fastify') 7 | const { request } = require('undici') 8 | const From = require('..') 9 | const Multipart = require('@fastify/multipart') 10 | const http = require('node:http') 11 | const FormData = require('form-data') 12 | 13 | const split = require('split2') 14 | const logStream = split(JSON.parse) 15 | 16 | const instance = Fastify({ 17 | logger: { 18 | level: 'warn', 19 | stream: logStream 20 | } 21 | }) 22 | 23 | instance.register(Multipart) 24 | instance.register(From) 25 | 26 | t.test('fastify-multipart-incompatibility', async (t) => { 27 | t.plan(9) 28 | 29 | t.after(() => instance.close()) 30 | 31 | const filetPath = path.join(__dirname, 'fixtures', 'file.txt') 32 | const fileContent = fs.readFileSync(filetPath, { encoding: 'utf-8' }) 33 | 34 | const target = http.createServer((req, res) => { 35 | t.assert.ok('request proxied') 36 | t.assert.strictEqual(req.method, 'POST') 37 | t.assert.match(req.headers['content-type'], /^multipart\/form-data/) 38 | let data = '' 39 | req.setEncoding('utf8') 40 | req.on('data', (d) => { 41 | data += d 42 | }) 43 | req.on('end', () => { 44 | t.assert.notDeepEqual(data, 'Content-Disposition: form-data; name="key"') 45 | t.assert.notDeepEqual(data, 'value') 46 | t.assert.notDeepEqual(data, 'Content-Disposition: form-data; name="file"') 47 | t.assert.notDeepEqual(data, fileContent) 48 | res.setHeader('content-type', 'application/json') 49 | res.statusCode = 200 50 | res.end(JSON.stringify({ something: 'else' })) 51 | }) 52 | }) 53 | 54 | instance.post('/', (_request, reply) => { 55 | reply.from(`http://localhost:${target.address().port}`) 56 | }) 57 | 58 | t.after(() => target.close()) 59 | 60 | await new Promise(resolve => instance.listen({ port: 0 }, resolve)) 61 | 62 | logStream.on('data', (log) => { 63 | if ( 64 | log.level === 40 && 65 | log.msg.match(/@fastify\/reply-from might not behave as expected when used with @fastify\/multipart/) 66 | ) { 67 | t.assert.ok('incompatibility warn message logged') 68 | } 69 | }) 70 | 71 | await new Promise(resolve => target.listen({ port: 0 }, resolve)) 72 | 73 | const form = new FormData() 74 | form.append('key', 'value') 75 | form.append('file', fs.createReadStream(filetPath, { encoding: 'utf-8' })) 76 | 77 | const result = await request(`http://localhost:${instance.server.address().port}`, { 78 | method: 'POST', 79 | headers: form.getHeaders(), 80 | body: form 81 | }) 82 | 83 | t.assert.deepStrictEqual(await result.body.json(), { something: 'else' }) 84 | }) 85 | -------------------------------------------------------------------------------- /test/fix-GHSA-v2v2-hph8-q5xp.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { describe, after, it } = require('node:test') 4 | const fastify = require('fastify') 5 | const { request } = require('undici') 6 | const fastifyProxyFrom = require('..') 7 | const { isIPv6 } = require('node:net') 8 | 9 | describe('GHSA-v2v2-hph8-q5xp', function () { 10 | it('should not parse the body if it is an object', async function (t) { 11 | t.plan(1) 12 | 13 | const upstream = fastify() 14 | 15 | upstream.post('/test', async (request) => { 16 | if (typeof request.body === 'object') { 17 | return 'not ok' 18 | } 19 | return 'ok' 20 | }) 21 | 22 | await upstream.listen({ port: 0 }) 23 | 24 | let upstreamAdress = upstream.server.address().address 25 | 26 | if (isIPv6(upstreamAdress)) { 27 | upstreamAdress = `[${upstreamAdress}]` 28 | } 29 | 30 | const app = fastify() 31 | app.register(fastifyProxyFrom) 32 | 33 | app.post('/test', (request, reply) => { 34 | if (request.body.method === 'invalid_method') { 35 | return reply.code(400).send({ message: 'payload contains invalid method' }) 36 | } 37 | reply.from(`http://${upstreamAdress}:${upstream.server.address().port}/test`) 38 | }) 39 | 40 | await app.listen({ port: 0 }) 41 | 42 | after(() => { 43 | upstream.close() 44 | app.close() 45 | }) 46 | 47 | let appAddress = app.server.address().address 48 | 49 | if (isIPv6(appAddress)) { 50 | appAddress = `[${appAddress}]` 51 | } 52 | 53 | const response = await request( 54 | `http://${appAddress}:${app.server.address().port}/test`, 55 | { 56 | headers: { 'content-type': 'application/json ; charset=utf-8' }, 57 | // eslint-disable-next-line no-useless-escape 58 | body: '"{\\\"method\\\":\\\"invalid_method\\\"}"', 59 | method: 'POST' 60 | }) 61 | 62 | t.assert.strictEqual(await response.body.text(), 'ok') 63 | }) 64 | }) 65 | -------------------------------------------------------------------------------- /test/fixtures/fastify.cert: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDBzCCAe+gAwIBAgIJALbQMeb7k/WqMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNV 3 | BAMMD3d3dy5mYXN0aWZ5Lm9yZzAeFw0xNzAyMDcyMDE5NDJaFw0yNzAyMDUyMDE5 4 | NDJaMBoxGDAWBgNVBAMMD3d3dy5mYXN0aWZ5Lm9yZzCCASIwDQYJKoZIhvcNAQEB 5 | BQADggEPADCCAQoCggEBAKtfXzDMmU+n3A7oVVOiqp6Z5cgu1t+qgj7TadwXONvO 6 | RZvuOcE8BZpM9tQEDE5XEIdcszDx0tWKHHSobgZAxDaEuK1PMhh/RTNvw1KzYJFm 7 | 2G38mqgm11JUni87xmIFqpgJfeCApHnWUv+3/npuQniOoVSL13jdXEifeFM8onQn 8 | R73TVDyvMOjljTulMo0n9V8pYhVSzPnm2uxTu03p5+HosQE2bU0QKj7k8/8dwRVX 9 | EqnTtbLoW+Wf7V2W3cr/UnfPH8JSaBWTqct0pgXqYIqOSTiWQkO7pE69mGPHrRlm 10 | 7+whp4WRriTacB3Ul+Cbx28wHU+D83ver4A8LKGVDSECAwEAAaNQME4wHQYDVR0O 11 | BBYEFHVzTr/tNziIUrR75UHXXA84yqmgMB8GA1UdIwQYMBaAFHVzTr/tNziIUrR7 12 | 5UHXXA84yqmgMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAKVSdGeF 13 | vYcZOi0TG2WX7O3tSmu4G4nGxTldFiEVF89G0AU+HhNy9iwKXQLjDB7zMe/ZKbtJ 14 | cQgc6s8eZWxBk/OoPD1WNFGstx2EO2kRkSUBKhwnOct7CIS5X+NPXyHx2Yi03JHX 15 | unMA4WaHyo0dK4vAuali4OYdQqajNwL74avkRIxXFnZQeHzaq6tc6gX+ryB4dDSr 16 | tYn46Lo14D5jH6PtZ8DlGK+jIzM4IE7TEp2iv0CgaTU4ryt/SHPnLxfwZUpl7gSO 17 | EqkMAy3TlRMpv0oXM2Vh/CsyJzq2P/nY/O3bolsashSPWo9WsQTH4giYVA51ZVDK 18 | lGksQD+oWpfa3X0= 19 | -----END CERTIFICATE----- 20 | -------------------------------------------------------------------------------- /test/fixtures/fastify.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpQIBAAKCAQEAq19fMMyZT6fcDuhVU6KqnpnlyC7W36qCPtNp3Bc4285Fm+45 3 | wTwFmkz21AQMTlcQh1yzMPHS1YocdKhuBkDENoS4rU8yGH9FM2/DUrNgkWbYbfya 4 | qCbXUlSeLzvGYgWqmAl94ICkedZS/7f+em5CeI6hVIvXeN1cSJ94UzyidCdHvdNU 5 | PK8w6OWNO6UyjSf1XyliFVLM+eba7FO7Tenn4eixATZtTRAqPuTz/x3BFVcSqdO1 6 | suhb5Z/tXZbdyv9Sd88fwlJoFZOpy3SmBepgio5JOJZCQ7ukTr2YY8etGWbv7CGn 7 | hZGuJNpwHdSX4JvHbzAdT4Pze96vgDwsoZUNIQIDAQABAoIBAG278ys/R8he1yVg 8 | lgqo9ZH7P8zwWTz9ZMsv+vAomor9SUtwvuDCO2AzejYGpY6gZ4AV1tQ3dOaxukjk 9 | 9Rbh8AJs+AhZ1t0i2b/3B95z6BkS/vFmt+2GeYhJkMT0BLMNp9AU+9p+5VLy71C5 10 | k6T3525k/l8x8HZ/YDFMk/LQt8GhvM6A3J3BNElKraiDVO6ZIWgQQ5wiefJkApo1 11 | BsptHNTx83FbnkEbAahmOR8PfKcRdKY/mZDM2WrlfoU2uwVzPV0/KdYucpsfg2et 12 | jb5bdJzcvZDuDF4GsPi1asCSC1c403R0XGuPFW9TiBuOPxbfhYK2o60yTggX6H2X 13 | 39WBc/ECgYEA3KNGgXEWzDSLpGciUisP+MzulOdQPawBTUHNykpQklEppnZbNWCX 14 | 07dv6uasnp0pFHG4WlhZJ4+IQBpZH6xAVy9y68PvN7IDYdgMiEiYPSyqQu0rvJGa 15 | 2ZR79SHDokZ8K5oofocC839RzleNRqWqxIwhHt29sxVs73kvml6OQm0CgYEAxtbA 16 | zbQwf6DXtFwutSgfOLgdXQK72beBdyeTcpUGbkonl5xHSbtz0CFmRpKiPnXfgg4W 17 | GXlTrqlYF/o048B7dU9+jCKY5DXx1Yzg/EFisEIClad3WXMhNOz1vBYVH6xU3Zq1 18 | YuYr5dcqiCWDv89e6Y6WJOhwIDZi6RqikD2EJQUCgYEAnWSAJFCnIa8OOo4z5oe/ 19 | kg2m2GQWUphEKXeatQbEaUwquQvPTsmEJUzDMr+xPkkAiAwDpbdGijkSyh/Bmh2H 20 | nGpFwbf5CzMaxI6ZihK3P1SAdNO5koAQBcytjJW0eCtt4rDK2E+5pDgcBGVia5Y8 21 | to78BYfLDlhnaIF7mtR/CRUCgYEAvGCuzvOcUv4F/eirk5NMaQb9QqYZZD2XWVTU 22 | O2T2b7yvX9J+M1t1cESESe4X6cbwlp1T0JSCdGIZhLXWL8Om80/52zfX07VLxP6w 23 | FCy6G7SeEDxVNRh+6E5qzOO65YP17vDoUacxBZJgyBWKiUkkaW9dzd+sgsgj0yYZ 24 | xz+QlyUCgYEAxdNWQnz0pR5Rt2dbIedPs7wmiZ7eAe0VjCdhMa52IyJpejdeB6Bn 25 | Es+3lkHr0Xzty8XlQZcpbswhM8UZRgPVoBvvwQdQbv5yV+LdUu69pLM7InsdZy8u 26 | opPY/+q9lRdJt4Pbep3pOWYeLP7k5l4vei2vOEMHRjHnoqM5etSb6RU= 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /test/fixtures/file.txt: -------------------------------------------------------------------------------- 1 | file content -------------------------------------------------------------------------------- /test/full-delete-http2.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const Fastify = require('fastify') 5 | const { request, Agent } = require('undici') 6 | const From = require('..') 7 | 8 | test('http -> http2', async function (t) { 9 | const instance = Fastify() 10 | 11 | t.after(() => instance.close()) 12 | 13 | const target = Fastify({ 14 | http2: true 15 | }) 16 | 17 | target.delete('/', (_request, reply) => { 18 | t.assert.ok('request proxied') 19 | reply.code(200).header('x-my-header', 'hello!').send({ 20 | hello: 'world' 21 | }) 22 | }) 23 | 24 | instance.delete('/', (_request, reply) => { 25 | reply.from() 26 | }) 27 | 28 | t.after(() => target.close()) 29 | 30 | await target.listen({ port: 0 }) 31 | 32 | instance.register(From, { 33 | base: `http://localhost:${target.server.address().port}`, 34 | http2: true 35 | }) 36 | 37 | await instance.listen({ port: 0 }) 38 | 39 | const { headers, body, statusCode } = await request( 40 | `http://localhost:${instance.server.address().port}`, 41 | { 42 | method: 'DELETE', 43 | responseType: 'json', 44 | dispatcher: new Agent({ pipelining: 0 }) 45 | } 46 | ) 47 | t.assert.strictEqual(statusCode, 200) 48 | t.assert.strictEqual(headers['x-my-header'], 'hello!') 49 | t.assert.match(headers['content-type'], /application\/json/) 50 | t.assert.deepStrictEqual(await body.json(), { hello: 'world' }) 51 | instance.close() 52 | target.close() 53 | }) 54 | -------------------------------------------------------------------------------- /test/full-get-test.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const t = require('node:test') 4 | const Fastify = require('fastify') 5 | const { request } = require('undici') 6 | const From = require('..') 7 | const http = require('node:http') 8 | 9 | const instance = Fastify() 10 | instance.register(From) 11 | 12 | t.test('full get', async (t) => { 13 | t.plan(7) 14 | t.after(() => instance.close()) 15 | 16 | const target = http.createServer((req, res) => { 17 | t.assert.ok('request proxied') 18 | t.assert.strictEqual(req.method, 'GET') 19 | t.assert.strictEqual(req.url, '/hello') 20 | res.statusCode = 205 21 | res.setHeader('Content-Type', 'text/plain') 22 | res.setHeader('x-my-header', 'hello!') 23 | res.end('hello world') 24 | }) 25 | 26 | instance.get('/', (_request, reply) => { 27 | reply.from(`http://localhost:${target.address().port}/hello`) 28 | }) 29 | 30 | t.after(() => target.close()) 31 | 32 | await new Promise(resolve => instance.listen({ port: 0 }, resolve)) 33 | 34 | await new Promise(resolve => target.listen({ port: 0 }, resolve)) 35 | 36 | const result = await request(`http://localhost:${instance.server.address().port}`) 37 | 38 | t.assert.strictEqual(result.headers['content-type'], 'text/plain') 39 | t.assert.strictEqual(result.headers['x-my-header'], 'hello!') 40 | t.assert.strictEqual(result.statusCode, 205) 41 | t.assert.strictEqual(await result.body.text(), 'hello world') 42 | }) 43 | -------------------------------------------------------------------------------- /test/full-https-get.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const t = require('node:test') 4 | const Fastify = require('fastify') 5 | const { request, Agent } = require('undici') 6 | const From = require('..') 7 | const https = require('node:https') 8 | const fs = require('node:fs') 9 | const path = require('node:path') 10 | const certs = { 11 | key: fs.readFileSync(path.join(__dirname, 'fixtures', 'fastify.key')), 12 | cert: fs.readFileSync(path.join(__dirname, 'fixtures', 'fastify.cert')) 13 | } 14 | 15 | const instance = Fastify({ 16 | https: certs 17 | }) 18 | instance.register(From) 19 | 20 | t.test('full-https-get', async (t) => { 21 | t.plan(6) 22 | t.after(() => instance.close()) 23 | 24 | const target = https.createServer(certs, (req, res) => { 25 | t.assert.ok('request proxied') 26 | t.assert.strictEqual(req.method, 'GET') 27 | res.statusCode = 205 28 | res.setHeader('Content-Type', 'text/plain') 29 | res.setHeader('x-my-header', 'hello!') 30 | res.end('hello world') 31 | }) 32 | 33 | instance.get('/', (_request, reply) => { 34 | reply.from(`https://localhost:${target.address().port}`) 35 | }) 36 | 37 | t.after(() => target.close()) 38 | 39 | await new Promise(resolve => instance.listen({ port: 0 }, resolve)) 40 | 41 | await new Promise(resolve => target.listen({ port: 0 }, resolve)) 42 | 43 | const result = await request(`https://localhost:${instance.server.address().port}`, { 44 | dispatcher: new Agent({ 45 | connect: { 46 | rejectUnauthorized: false 47 | } 48 | }) 49 | }) 50 | 51 | t.assert.strictEqual(result.headers['content-type'], 'text/plain') 52 | t.assert.strictEqual(result.headers['x-my-header'], 'hello!') 53 | t.assert.strictEqual(result.statusCode, 205) 54 | t.assert.strictEqual(await result.body.text(), 'hello world') 55 | }) 56 | -------------------------------------------------------------------------------- /test/full-post-extended-content-type.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const t = require('node:test') 4 | const Fastify = require('fastify') 5 | const { request } = require('undici') 6 | const From = require('..') 7 | const http = require('node:http') 8 | 9 | const instance = Fastify() 10 | instance.register(From) 11 | 12 | t.test('full post extended content type', async (t) => { 13 | t.plan(6) 14 | t.after(() => instance.close()) 15 | 16 | const target = http.createServer((req, res) => { 17 | t.assert.ok('request proxied') 18 | t.assert.strictEqual(req.method, 'POST') 19 | t.assert.strictEqual(req.headers['content-type'].startsWith('application/json'), true) 20 | let data = '' 21 | req.setEncoding('utf8') 22 | req.on('data', (d) => { 23 | data += d 24 | }) 25 | req.on('end', () => { 26 | t.assert.deepStrictEqual(JSON.parse(data), { hello: 'world' }) 27 | res.statusCode = 200 28 | res.setHeader('content-type', 'application/json') 29 | res.end(JSON.stringify({ something: 'else' })) 30 | }) 31 | }) 32 | 33 | instance.post('/', (_request, reply) => { 34 | reply.from(`http://localhost:${target.address().port}`) 35 | }) 36 | 37 | t.after(() => target.close()) 38 | 39 | await new Promise(resolve => instance.listen({ port: 0 }, resolve)) 40 | 41 | await new Promise(resolve => target.listen({ port: 0 }, resolve)) 42 | 43 | const result = await request(`http://localhost:${instance.server.address().port}`, { 44 | method: 'POST', 45 | body: JSON.stringify({ 46 | hello: 'world' 47 | }), 48 | headers: { 49 | 'content-type': 'application/json;charset=utf-8' 50 | } 51 | }) 52 | 53 | t.assert.strictEqual(result.headers['content-type'], 'application/json') 54 | t.assert.deepStrictEqual(await result.body.json(), { something: 'else' }) 55 | }) 56 | -------------------------------------------------------------------------------- /test/full-post-http2.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const Fastify = require('fastify') 5 | const From = require('..') 6 | const { request, Agent } = require('undici') 7 | 8 | test('http -> http2', async function (t) { 9 | const instance = Fastify() 10 | 11 | t.after(() => instance.close()) 12 | 13 | const target = Fastify({ 14 | http2: true 15 | }) 16 | 17 | target.post('/', (request, reply) => { 18 | t.assert.ok('request proxied') 19 | t.assert.deepStrictEqual(request.body, { something: 'else' }) 20 | reply.code(200).header('x-my-header', 'hello!').send({ 21 | hello: 'world' 22 | }) 23 | }) 24 | 25 | instance.post('/', (_request, reply) => { 26 | reply.from() 27 | }) 28 | 29 | t.after(() => target.close()) 30 | 31 | await target.listen({ port: 0 }) 32 | 33 | instance.register(From, { 34 | base: `http://localhost:${target.server.address().port}`, 35 | http2: true 36 | }) 37 | 38 | await instance.listen({ port: 0 }) 39 | 40 | const { headers, body, statusCode } = await request(`http://localhost:${instance.server.address().port}`, { 41 | method: 'POST', 42 | body: JSON.stringify({ something: 'else' }), 43 | headers: { 44 | 'content-type': 'application/json' 45 | }, 46 | dispatcher: new Agent({ 47 | pipelining: 0 48 | }) 49 | }) 50 | t.assert.strictEqual(statusCode, 200) 51 | t.assert.strictEqual(headers['x-my-header'], 'hello!') 52 | t.assert.match(headers['content-type'], /application\/json/) 53 | t.assert.deepStrictEqual(await body.json(), { hello: 'world' }) 54 | instance.close() 55 | target.close() 56 | }) 57 | -------------------------------------------------------------------------------- /test/full-post-stream-core.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const t = require('node:test') 4 | const Fastify = require('fastify') 5 | const { request } = require('undici') 6 | const From = require('..') 7 | const http = require('node:http') 8 | 9 | const instance = Fastify() 10 | instance.register(From, { 11 | http: true 12 | }) 13 | 14 | t.test('full post stream core', async (t) => { 15 | t.plan(5) 16 | t.after(() => instance.close()) 17 | 18 | instance.addContentTypeParser('application/octet-stream', function (_req, payload, done) { 19 | done(null, payload) 20 | }) 21 | 22 | t.after(() => instance.close()) 23 | 24 | const target = http.createServer((req, res) => { 25 | t.assert.ok('request proxied') 26 | t.assert.strictEqual(req.method, 'POST') 27 | t.assert.strictEqual(req.headers['content-type'], 'application/octet-stream') 28 | let data = '' 29 | req.setEncoding('utf8') 30 | req.on('data', (d) => { 31 | data += d 32 | }) 33 | req.on('end', () => { 34 | t.assert.deepStrictEqual(JSON.parse(data), { hello: 'world' }) 35 | res.statusCode = 200 36 | res.setHeader('content-type', 'application/octet-stream') 37 | res.end(JSON.stringify({ something: 'else' })) 38 | }) 39 | }) 40 | 41 | instance.post('/', (_request, reply) => { 42 | reply.from(`http://localhost:${target.address().port}`) 43 | }) 44 | 45 | t.after(() => target.close()) 46 | 47 | await new Promise(resolve => instance.listen({ port: 0 }, resolve)) 48 | 49 | await new Promise(resolve => target.listen({ port: 0 }, resolve)) 50 | 51 | const result = await request(`http://localhost:${instance.server.address().port}`, { 52 | method: 'POST', 53 | headers: { 54 | 'content-type': 'application/octet-stream' 55 | }, 56 | body: JSON.stringify({ 57 | hello: 'world' 58 | }) 59 | }) 60 | 61 | t.assert.deepStrictEqual(await result.body.json(), { something: 'else' }) 62 | }) 63 | -------------------------------------------------------------------------------- /test/full-post-stream.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const t = require('node:test') 4 | const Fastify = require('fastify') 5 | const { request } = require('undici') 6 | const From = require('..') 7 | const http = require('node:http') 8 | 9 | const instance = Fastify() 10 | instance.register(From) 11 | 12 | t.test('full post stream', async (t) => { 13 | t.plan(5) 14 | t.after(() => instance.close()) 15 | 16 | instance.addContentTypeParser('application/octet-stream', function (_req, payload, done) { 17 | done(null, payload) 18 | }) 19 | 20 | t.after(() => instance.close()) 21 | 22 | const target = http.createServer((req, res) => { 23 | t.assert.ok('request proxied') 24 | t.assert.strictEqual(req.method, 'POST') 25 | t.assert.strictEqual(req.headers['content-type'], 'application/octet-stream') 26 | let data = '' 27 | req.setEncoding('utf8') 28 | req.on('data', (d) => { 29 | data += d 30 | }) 31 | req.on('end', () => { 32 | t.assert.deepStrictEqual(JSON.parse(data), { hello: 'world' }) 33 | res.statusCode = 200 34 | res.setHeader('content-type', 'application/octet-stream') 35 | res.end(JSON.stringify({ something: 'else' })) 36 | }) 37 | }) 38 | 39 | instance.post('/', (_request, reply) => { 40 | reply.from(`http://localhost:${target.address().port}`) 41 | }) 42 | 43 | t.after(() => target.close()) 44 | 45 | await new Promise(resolve => instance.listen({ port: 0 }, resolve)) 46 | 47 | await new Promise(resolve => target.listen({ port: 0 }, resolve)) 48 | 49 | const result = await request(`http://localhost:${instance.server.address().port}`, { 50 | method: 'POST', 51 | headers: { 52 | 'content-type': 'application/octet-stream' 53 | }, 54 | body: JSON.stringify({ 55 | hello: 'world' 56 | }) 57 | }) 58 | 59 | t.assert.deepStrictEqual(await result.body.json(), { something: 'else' }) 60 | }) 61 | -------------------------------------------------------------------------------- /test/full-post.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const t = require('node:test') 4 | const Fastify = require('fastify') 5 | const { request } = require('undici') 6 | const From = require('..') 7 | const http = require('node:http') 8 | 9 | const instance = Fastify() 10 | instance.register(From) 11 | 12 | t.test('full post', async (t) => { 13 | t.plan(5) 14 | t.after(() => instance.close()) 15 | 16 | const target = http.createServer((req, res) => { 17 | t.assert.ok('request proxied') 18 | t.assert.strictEqual(req.method, 'POST') 19 | t.assert.strictEqual(req.headers['content-type'], 'application/json') 20 | let data = '' 21 | req.setEncoding('utf8') 22 | req.on('data', (d) => { 23 | data += d 24 | }) 25 | req.on('end', () => { 26 | t.assert.deepStrictEqual(JSON.parse(data), { hello: 'world' }) 27 | res.statusCode = 200 28 | res.setHeader('content-type', 'application/json') 29 | res.end(JSON.stringify({ something: 'else' })) 30 | }) 31 | }) 32 | 33 | instance.post('/', (_request, reply) => { 34 | reply.from(`http://localhost:${target.address().port}`) 35 | }) 36 | 37 | t.after(() => target.close()) 38 | 39 | await new Promise(resolve => instance.listen({ port: 0 }, resolve)) 40 | 41 | await new Promise(resolve => target.listen({ port: 0 }, resolve)) 42 | 43 | const result = await request(`http://localhost:${instance.server.address().port}`, { 44 | method: 'POST', 45 | headers: { 46 | 'content-type': 'application/json' 47 | }, 48 | body: JSON.stringify({ 49 | hello: 'world' 50 | }) 51 | }) 52 | 53 | t.assert.deepStrictEqual(await result.body.json(), { something: 'else' }) 54 | }) 55 | -------------------------------------------------------------------------------- /test/full-querystring-rewrite-option-complex.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const t = require('node:test') 4 | const Fastify = require('fastify') 5 | const { request } = require('undici') 6 | const From = require('..') 7 | const http = require('node:http') 8 | 9 | const instance = Fastify() 10 | 11 | t.test('full querystring rewrite option complex', async (t) => { 12 | t.plan(7) 13 | t.after(() => instance.close()) 14 | 15 | const target = http.createServer((req, res) => { 16 | t.assert.ok('request proxied') 17 | t.assert.strictEqual(req.method, 'GET') 18 | t.assert.strictEqual(req.url, '/world?b=c') 19 | res.statusCode = 205 20 | res.setHeader('Content-Type', 'text/plain') 21 | res.setHeader('x-my-header', 'hello!') 22 | res.end('hello world') 23 | }) 24 | 25 | instance.get('/hello', (_request, reply) => { 26 | reply.from(`http://localhost:${target.address().port}/world?a=b`, { 27 | queryString: { b: 'c' } 28 | }) 29 | }) 30 | 31 | t.after(() => target.close()) 32 | 33 | await new Promise(resolve => target.listen({ port: 0 }, resolve)) 34 | 35 | instance.register(From) 36 | 37 | await new Promise(resolve => instance.listen({ port: 0 }, resolve)) 38 | 39 | const result = await request(`http://localhost:${instance.server.address().port}/hello?a=b`) 40 | t.assert.strictEqual(result.headers['content-type'], 'text/plain') 41 | t.assert.strictEqual(result.headers['x-my-header'], 'hello!') 42 | t.assert.strictEqual(result.statusCode, 205) 43 | t.assert.strictEqual(await result.body.text(), 'hello world') 44 | }) 45 | -------------------------------------------------------------------------------- /test/full-querystring-rewrite-option-function-request.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const t = require('node:test') 4 | const Fastify = require('fastify') 5 | const { request } = require('undici') 6 | const From = require('..') 7 | const http = require('node:http') 8 | const querystring = require('node:querystring') 9 | 10 | const instance = Fastify() 11 | 12 | instance.addHook('preHandler', (request, _reply, done) => { 13 | request.addedVal = 'test' 14 | done() 15 | }) 16 | 17 | t.test('full querystring rewrite option function request', async (t) => { 18 | t.plan(7) 19 | t.after(() => instance.close()) 20 | 21 | const target = http.createServer((req, res) => { 22 | t.assert.ok('request proxied') 23 | t.assert.strictEqual(req.method, 'GET') 24 | t.assert.strictEqual(req.url, '/world?q=test') 25 | res.statusCode = 205 26 | res.setHeader('Content-Type', 'text/plain') 27 | res.setHeader('x-my-header', 'hello!') 28 | res.end('hello world') 29 | }) 30 | 31 | instance.get('/hello', (_request, reply) => { 32 | reply.from(`http://localhost:${target.address().port}/world?a=b`, { 33 | queryString (_search, _reqUrl, request) { 34 | return querystring.stringify({ q: request.addedVal }) 35 | } 36 | }) 37 | }) 38 | 39 | t.after(() => target.close()) 40 | 41 | await new Promise(resolve => target.listen({ port: 0 }, resolve)) 42 | 43 | instance.register(From) 44 | 45 | await new Promise(resolve => instance.listen({ port: 0 }, resolve)) 46 | 47 | const result = await request(`http://localhost:${instance.server.address().port}/hello?a=b`) 48 | t.assert.strictEqual(result.headers['content-type'], 'text/plain') 49 | t.assert.strictEqual(result.headers['x-my-header'], 'hello!') 50 | t.assert.strictEqual(result.statusCode, 205) 51 | t.assert.strictEqual(await result.body.text(), 'hello world') 52 | }) 53 | -------------------------------------------------------------------------------- /test/full-querystring-rewrite-option-function.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const t = require('node:test') 4 | const Fastify = require('fastify') 5 | const { request } = require('undici') 6 | const From = require('..') 7 | const http = require('node:http') 8 | const querystring = require('node:querystring') 9 | 10 | const instance = Fastify() 11 | 12 | t.test('full querystring rewrite option function', async (t) => { 13 | t.plan(7) 14 | t.after(() => instance.close()) 15 | 16 | const target = http.createServer((req, res) => { 17 | t.assert.ok('request proxied') 18 | t.assert.strictEqual(req.method, 'GET') 19 | t.assert.strictEqual(req.url, '/world?b=c') 20 | res.statusCode = 205 21 | res.setHeader('Content-Type', 'text/plain') 22 | res.setHeader('x-my-header', 'hello!') 23 | res.end('hello world') 24 | }) 25 | 26 | instance.get('/hello', (_request, reply) => { 27 | reply.from(`http://localhost:${target.address().port}/world?a=b`, { 28 | queryString () { 29 | return querystring.stringify({ b: 'c' }) 30 | } 31 | }) 32 | }) 33 | 34 | t.after(() => target.close()) 35 | 36 | await new Promise(resolve => target.listen({ port: 0 }, resolve)) 37 | 38 | instance.register(From) 39 | 40 | await new Promise(resolve => instance.listen({ port: 0 }, resolve)) 41 | 42 | const result = await request(`http://localhost:${instance.server.address().port}/hello?a=b`) 43 | 44 | t.assert.strictEqual(result.headers['content-type'], 'text/plain') 45 | t.assert.strictEqual(result.headers['x-my-header'], 'hello!') 46 | t.assert.strictEqual(result.statusCode, 205) 47 | t.assert.strictEqual(await result.body.text(), 'hello world') 48 | }) 49 | -------------------------------------------------------------------------------- /test/full-querystring-rewrite-option.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const t = require('node:test') 4 | const Fastify = require('fastify') 5 | const { request } = require('undici') 6 | const From = require('..') 7 | const http = require('node:http') 8 | 9 | const instance = Fastify() 10 | 11 | t.test('full querystring rewrite option', async (t) => { 12 | t.plan(7) 13 | t.after(() => instance.close()) 14 | 15 | const target = http.createServer((req, res) => { 16 | t.assert.ok('request proxied') 17 | t.assert.strictEqual(req.method, 'GET') 18 | t.assert.strictEqual(req.url, '/world?b=c') 19 | res.statusCode = 205 20 | res.setHeader('Content-Type', 'text/plain') 21 | res.setHeader('x-my-header', 'hello!') 22 | res.end('hello world') 23 | }) 24 | 25 | instance.get('/hello', (_request, reply) => { 26 | reply.from(`http://localhost:${target.address().port}/world`, { 27 | queryString: { b: 'c' } 28 | }) 29 | }) 30 | 31 | t.after(() => target.close()) 32 | 33 | await new Promise(resolve => target.listen({ port: 0 }, resolve)) 34 | 35 | instance.register(From) 36 | 37 | await new Promise(resolve => instance.listen({ port: 0 }, resolve)) 38 | 39 | const result = await request(`http://localhost:${instance.server.address().port}/hello?a=b`) 40 | 41 | t.assert.strictEqual(result.headers['content-type'], 'text/plain') 42 | t.assert.strictEqual(result.headers['x-my-header'], 'hello!') 43 | t.assert.strictEqual(result.statusCode, 205) 44 | t.assert.strictEqual(await result.body.text(), 'hello world') 45 | }) 46 | -------------------------------------------------------------------------------- /test/full-querystring-rewrite-string.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const t = require('node:test') 4 | const Fastify = require('fastify') 5 | const { request } = require('undici') 6 | const From = require('..') 7 | const http = require('node:http') 8 | 9 | const instance = Fastify() 10 | 11 | t.test('full querystring rewrite string', async (t) => { 12 | t.plan(7) 13 | t.after(() => instance.close()) 14 | 15 | const target = http.createServer((req, res) => { 16 | t.assert.ok('request proxied') 17 | t.assert.strictEqual(req.method, 'GET') 18 | t.assert.strictEqual(req.url, '/world?b=c') 19 | res.statusCode = 205 20 | res.setHeader('Content-Type', 'text/plain') 21 | res.setHeader('x-my-header', 'hello!') 22 | res.end('hello world') 23 | }) 24 | 25 | instance.get('/hello', (_request, reply) => { 26 | reply.from(`http://localhost:${target.address().port}/world?b=c`) 27 | }) 28 | 29 | t.after(() => target.close()) 30 | 31 | await new Promise(resolve => target.listen({ port: 0 }, resolve)) 32 | 33 | instance.register(From) 34 | 35 | await new Promise(resolve => instance.listen({ port: 0 }, resolve)) 36 | 37 | const result = await request(`http://localhost:${instance.server.address().port}/hello?a=b`) 38 | 39 | t.assert.strictEqual(result.headers['content-type'], 'text/plain') 40 | t.assert.strictEqual(result.headers['x-my-header'], 'hello!') 41 | t.assert.strictEqual(result.statusCode, 205) 42 | t.assert.strictEqual(await result.body.text(), 'hello world') 43 | }) 44 | -------------------------------------------------------------------------------- /test/full-querystring-rewrite.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const t = require('node:test') 4 | const Fastify = require('fastify') 5 | const { request } = require('undici') 6 | const From = require('..') 7 | const http = require('node:http') 8 | 9 | const instance = Fastify() 10 | 11 | t.test('full querystring rewrite', async (t) => { 12 | t.plan(7) 13 | t.after(() => instance.close()) 14 | 15 | const target = http.createServer((req, res) => { 16 | t.assert.ok('request proxied') 17 | t.assert.strictEqual(req.method, 'GET') 18 | t.assert.strictEqual(req.url, '/world?a=b') 19 | res.statusCode = 205 20 | res.setHeader('Content-Type', 'text/plain') 21 | res.setHeader('x-my-header', 'hello!') 22 | res.end('hello world') 23 | }) 24 | 25 | instance.get('/hello', (_request, reply) => { 26 | reply.from(`http://localhost:${target.address().port}/world`) 27 | }) 28 | 29 | t.after(() => target.close()) 30 | 31 | await new Promise(resolve => target.listen({ port: 0 }, resolve)) 32 | 33 | instance.register(From) 34 | 35 | await new Promise(resolve => instance.listen({ port: 0 }, resolve)) 36 | 37 | const result = await request(`http://localhost:${instance.server.address().port}/hello?a=b`) 38 | 39 | t.assert.strictEqual(result.headers['content-type'], 'text/plain') 40 | t.assert.strictEqual(result.headers['x-my-header'], 'hello!') 41 | t.assert.strictEqual(result.statusCode, 205) 42 | t.assert.strictEqual(await result.body.text(), 'hello world') 43 | }) 44 | -------------------------------------------------------------------------------- /test/full-querystring.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const t = require('node:test') 4 | const Fastify = require('fastify') 5 | const { request } = require('undici') 6 | const From = require('..') 7 | const http = require('node:http') 8 | 9 | const instance = Fastify() 10 | 11 | t.test('full querystring rewrite', async (t) => { 12 | t.plan(7) 13 | t.after(() => instance.close()) 14 | 15 | const target = http.createServer((req, res) => { 16 | t.assert.ok('request proxied') 17 | t.assert.strictEqual(req.method, 'GET') 18 | t.assert.strictEqual(req.url, '/world?a=b') 19 | res.statusCode = 205 20 | res.setHeader('Content-Type', 'text/plain') 21 | res.setHeader('x-my-header', 'hello!') 22 | res.end('hello world') 23 | }) 24 | 25 | instance.get('/hello', (_request, reply) => { 26 | reply.from(`http://localhost:${target.address().port}/world`) 27 | }) 28 | 29 | t.after(() => target.close()) 30 | 31 | await new Promise(resolve => target.listen({ port: 0 }, resolve)) 32 | 33 | instance.register(From) 34 | 35 | await new Promise(resolve => instance.listen({ port: 0 }, resolve)) 36 | 37 | const result = await request(`http://localhost:${instance.server.address().port}/hello?a=b`) 38 | 39 | t.assert.strictEqual(result.headers['content-type'], 'text/plain') 40 | t.assert.strictEqual(result.headers['x-my-header'], 'hello!') 41 | t.assert.strictEqual(result.statusCode, 205) 42 | t.assert.strictEqual(await result.body.text(), 'hello world') 43 | }) 44 | -------------------------------------------------------------------------------- /test/full-rewrite-body-content-type.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const t = require('node:test') 4 | const Fastify = require('fastify') 5 | const { request } = require('undici') 6 | const fastifyReplyFrom = require('..') 7 | const http = require('node:http') 8 | 9 | const instance = Fastify() 10 | instance.register(fastifyReplyFrom) 11 | 12 | const payload = { hello: 'world' } 13 | const msgPackPayload = Buffer.from([0x81, 0xa5, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0xa5, 0x77, 0x6f, 0x72, 0x6c, 0x64]) 14 | 15 | t.test('full rewrite body content-type', async (t) => { 16 | t.plan(6) 17 | t.after(() => instance.close()) 18 | 19 | const target = http.createServer((req, res) => { 20 | t.assert.ok('request proxied') 21 | t.assert.strictEqual(req.method, 'POST') 22 | t.assert.strictEqual(req.headers['content-type'], 'application/msgpack') 23 | const data = [] 24 | req.on('data', (d) => { 25 | data.push(d) 26 | }) 27 | req.on('end', () => { 28 | t.assert.deepStrictEqual(Buffer.concat(data), msgPackPayload) 29 | res.statusCode = 200 30 | res.setHeader('content-type', 'application/json') 31 | res.end(JSON.stringify({ something: 'else' })) 32 | }) 33 | }) 34 | 35 | instance.post('/', (request, reply) => { 36 | t.assert.deepStrictEqual(request.body, payload) 37 | reply.from(`http://localhost:${target.address().port}`, { 38 | contentType: 'application/msgpack', 39 | body: msgPackPayload 40 | }) 41 | }) 42 | 43 | t.after(() => target.close()) 44 | 45 | await new Promise(resolve => instance.listen({ port: 0 }, resolve)) 46 | 47 | await new Promise(resolve => target.listen({ port: 0 }, resolve)) 48 | 49 | const result = await request(`http://localhost:${instance.server.address().port}`, { 50 | method: 'POST', 51 | headers: { 52 | 'content-type': 'application/json' 53 | }, 54 | body: JSON.stringify({ hello: 'world' }), 55 | }) 56 | 57 | t.assert.deepStrictEqual(await result.body.json(), { something: 'else' }) 58 | }) 59 | -------------------------------------------------------------------------------- /test/full-rewrite-body-http.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const t = require('node:test') 4 | const Fastify = require('fastify') 5 | const { request } = require('undici') 6 | const From = require('..') 7 | const http = require('node:http') 8 | 9 | const instance = Fastify() 10 | instance.register(From, { 11 | http: true 12 | }) 13 | 14 | t.test('full rewrite body', async (t) => { 15 | t.plan(6) 16 | t.after(() => instance.close()) 17 | 18 | const target = http.createServer((req, res) => { 19 | t.assert.ok('request proxied') 20 | t.assert.strictEqual(req.method, 'POST') 21 | t.assert.strictEqual(req.headers['content-type'], 'application/json') 22 | t.assert.strictEqual(req.headers['content-length'], '20') 23 | let data = '' 24 | req.setEncoding('utf8') 25 | req.on('data', (d) => { 26 | data += d 27 | }) 28 | req.on('end', () => { 29 | t.assert.deepStrictEqual(JSON.parse(data), { something: 'else' }) 30 | res.statusCode = 200 31 | res.setHeader('content-type', 'application/json') 32 | res.end(JSON.stringify({ hello: 'fastify' })) 33 | }) 34 | }) 35 | 36 | instance.post('/', (_request, reply) => { 37 | reply.from(`http://localhost:${target.address().port}`, { 38 | body: { 39 | something: 'else' 40 | } 41 | }) 42 | }) 43 | 44 | t.after(() => target.close()) 45 | 46 | await new Promise(resolve => instance.listen({ port: 0 }, resolve)) 47 | 48 | await new Promise(resolve => target.listen({ port: 0 }, resolve)) 49 | 50 | const result = await request(`http://localhost:${instance.server.address().port}`, { 51 | method: 'POST', 52 | headers: { 53 | 'content-type': 'application/json' 54 | }, 55 | body: JSON.stringify({ hello: 'world' }), 56 | }) 57 | 58 | t.assert.deepStrictEqual(await result.body.json(), { hello: 'fastify' }) 59 | }) 60 | -------------------------------------------------------------------------------- /test/full-rewrite-body-to-empty-string.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const t = require('node:test') 4 | const Fastify = require('fastify') 5 | const { request } = require('undici') 6 | const From = require('..') 7 | const http = require('node:http') 8 | 9 | const instance = Fastify() 10 | instance.register(From, { 11 | http: true 12 | }) 13 | 14 | t.test('full rewrite body to empty string', async (t) => { 15 | t.plan(6) 16 | t.after(() => instance.close()) 17 | 18 | const target = http.createServer((req, res) => { 19 | t.assert.ok('request proxied') 20 | t.assert.strictEqual(req.method, 'POST') 21 | t.assert.strictEqual(req.headers['content-type'], 'application/json') 22 | t.assert.strictEqual(req.headers['content-length'], '2') 23 | let data = '' 24 | req.setEncoding('utf8') 25 | req.on('data', (d) => { 26 | data += d 27 | }) 28 | req.on('end', () => { 29 | t.assert.deepStrictEqual(JSON.parse(data), '') 30 | res.statusCode = 200 31 | res.setHeader('content-type', 'application/json') 32 | res.end(JSON.stringify({ hello: 'fastify' })) 33 | }) 34 | }) 35 | 36 | instance.post('/', (_request, reply) => { 37 | reply.from(`http://localhost:${target.address().port}`, { 38 | body: '' 39 | }) 40 | }) 41 | 42 | t.after(() => target.close()) 43 | 44 | await new Promise(resolve => instance.listen({ port: 0 }, resolve)) 45 | 46 | await new Promise(resolve => target.listen({ port: 0 }, resolve)) 47 | 48 | const result = await request(`http://localhost:${instance.server.address().port}`, { 49 | method: 'POST', 50 | headers: { 51 | 'content-type': 'application/json' 52 | }, 53 | body: JSON.stringify({ hello: 'world' }), 54 | }) 55 | 56 | t.assert.deepStrictEqual(await result.body.json(), { hello: 'fastify' }) 57 | }) 58 | -------------------------------------------------------------------------------- /test/full-rewrite-body-to-null.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const t = require('node:test') 4 | const Fastify = require('fastify') 5 | const { request } = require('undici') 6 | const From = require('..') 7 | const http = require('node:http') 8 | 9 | const instance = Fastify() 10 | instance.register(From, { 11 | http: true 12 | }) 13 | 14 | t.test('full rewrite body to null', async (t) => { 15 | t.plan(6) 16 | t.after(() => instance.close()) 17 | 18 | const target = http.createServer((req, res) => { 19 | t.assert.ok('request proxied') 20 | t.assert.strictEqual(req.method, 'POST') 21 | t.assert.ok(!('content-type' in req.headers)) 22 | t.assert.strictEqual(req.headers['content-length'], '0') 23 | let data = '' 24 | req.setEncoding('utf8') 25 | req.on('data', (d) => { 26 | data += d 27 | }) 28 | req.on('end', () => { 29 | t.assert.strictEqual(data.length, 0) 30 | res.statusCode = 200 31 | res.setHeader('content-type', 'application/json') 32 | res.end(JSON.stringify({ hello: 'fastify' })) 33 | }) 34 | }) 35 | 36 | instance.post('/', (_request, reply) => { 37 | reply.from(`http://localhost:${target.address().port}`, { 38 | body: null 39 | }) 40 | }) 41 | 42 | t.after(() => target.close()) 43 | 44 | await new Promise(resolve => instance.listen({ port: 0 }, resolve)) 45 | 46 | await new Promise(resolve => target.listen({ port: 0 }, resolve)) 47 | 48 | const result = await request(`http://localhost:${instance.server.address().port}`, { 49 | method: 'POST', 50 | headers: { 51 | 'content-type': 'application/json' 52 | }, 53 | body: JSON.stringify({ hello: 'world' }), 54 | }) 55 | 56 | t.assert.deepStrictEqual(await result.body.json(), { hello: 'fastify' }) 57 | }) 58 | -------------------------------------------------------------------------------- /test/full-rewrite-body.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const t = require('node:test') 4 | const Fastify = require('fastify') 5 | const { request } = require('undici') 6 | const From = require('..') 7 | const http = require('node:http') 8 | 9 | const instance = Fastify() 10 | instance.register(From) 11 | 12 | t.test('full rewrite body', async (t) => { 13 | t.plan(6) 14 | t.after(() => instance.close()) 15 | 16 | const target = http.createServer((req, res) => { 17 | t.assert.ok('request proxied') 18 | t.assert.strictEqual(req.method, 'POST') 19 | t.assert.strictEqual(req.headers['content-type'], 'application/json') 20 | t.assert.strictEqual(req.headers['content-length'], '20') 21 | let data = '' 22 | req.setEncoding('utf8') 23 | req.on('data', (d) => { 24 | data += d 25 | }) 26 | req.on('end', () => { 27 | t.assert.deepStrictEqual(JSON.parse(data), { something: 'else' }) 28 | res.statusCode = 200 29 | res.setHeader('content-type', 'application/json') 30 | res.end(JSON.stringify({ hello: 'fastify' })) 31 | }) 32 | }) 33 | 34 | instance.post('/', (_request, reply) => { 35 | reply.from(`http://localhost:${target.address().port}`, { 36 | body: { 37 | something: 'else' 38 | } 39 | }) 40 | }) 41 | 42 | t.after(() => target.close()) 43 | 44 | await new Promise(resolve => instance.listen({ port: 0 }, resolve)) 45 | 46 | await new Promise(resolve => target.listen({ port: 0 }, resolve)) 47 | 48 | const result = await request(`http://localhost:${instance.server.address().port}`, { 49 | method: 'POST', 50 | headers: { 51 | 'content-type': 'application/json' 52 | }, 53 | body: JSON.stringify({ hello: 'world' }), 54 | }) 55 | 56 | t.assert.deepStrictEqual(await result.body.json(), { hello: 'fastify' }) 57 | }) 58 | -------------------------------------------------------------------------------- /test/get-upstream-cache.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const t = require('node:test') 4 | const Fastify = require('fastify') 5 | const From = require('..') 6 | 7 | async function createTarget (i) { 8 | const target = Fastify({ 9 | keepAliveTimeout: 1 10 | }) 11 | 12 | target.get('/test', async () => { 13 | return `Hello from target ${i}` 14 | }) 15 | 16 | t.after(() => target.close()) 17 | await target.listen({ port: 3000 + i }) 18 | } 19 | 20 | async function run (t) { 21 | await Promise.all([ 22 | createTarget(1), 23 | createTarget(2) 24 | ]) 25 | 26 | const instance = Fastify({ 27 | keepAliveTimeout: 1 28 | }) 29 | 30 | instance.register(From, { 31 | base: 'http://localhost', 32 | http: true 33 | }) 34 | 35 | instance.get('/', (req, reply) => { 36 | const hostNumber = parseInt(req.headers['x-host-number']) 37 | const port = 3000 + hostNumber 38 | 39 | reply.from('/test', { 40 | getUpstream () { 41 | return `http://localhost:${port}` 42 | } 43 | }) 44 | }) 45 | 46 | t.after(() => instance.close()) 47 | await instance.listen({ port: 3000 }) 48 | 49 | const res1 = await instance.inject({ 50 | method: 'GET', 51 | url: '/', 52 | headers: { 53 | 'x-host-number': 1 54 | } 55 | }) 56 | t.assert.strictEqual(res1.statusCode, 200) 57 | t.assert.strictEqual(res1.body, 'Hello from target 1') 58 | 59 | const res2 = await instance.inject({ 60 | method: 'GET', 61 | url: '/', 62 | headers: { 63 | 'x-host-number': 2 64 | } 65 | }) 66 | t.assert.strictEqual(res2.statusCode, 200) 67 | t.assert.strictEqual(res2.body, 'Hello from target 2') 68 | } 69 | 70 | t.test('get-upstream-cache', async (t) => { 71 | t.plan(4) 72 | await run(t) 73 | }) 74 | -------------------------------------------------------------------------------- /test/get-upstream-http.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const t = require('node:test') 4 | const Fastify = require('fastify') 5 | const { request } = require('undici') 6 | const From = require('..') 7 | const http = require('node:http') 8 | 9 | const instance = Fastify() 10 | const instanceWithoutBase = Fastify() 11 | instance.register(From, { 12 | base: 'http://localhost', 13 | http: true, 14 | disableCache: true 15 | }) 16 | 17 | instanceWithoutBase.register(From, { 18 | http: true, 19 | disableCache: true 20 | }) 21 | 22 | t.test('getUpstream http', async (t) => { 23 | t.plan(8) 24 | t.after(() => instance.close()) 25 | t.after(() => instanceWithoutBase.close()) 26 | 27 | const target = http.createServer((req, res) => { 28 | t.assert.ok('request proxied') 29 | t.assert.strictEqual(req.method, 'GET') 30 | res.end(req.headers.host) 31 | }) 32 | 33 | instance.get('/test', (_request, reply) => { 34 | reply.from('/test', { 35 | getUpstream: (_req, base) => { 36 | t.assert.ok('getUpstream called') 37 | return `${base}:${target.address().port}` 38 | } 39 | }) 40 | }) 41 | 42 | instanceWithoutBase.get('/test2', (_request, reply) => { 43 | reply.from('/test2', { 44 | getUpstream: () => { 45 | t.assert.ok('getUpstream called') 46 | return `http://localhost:${target.address().port}` 47 | } 48 | }) 49 | }) 50 | 51 | t.after(() => target.close()) 52 | 53 | await new Promise(resolve => instance.listen({ port: 0 }, resolve)) 54 | 55 | await new Promise(resolve => instanceWithoutBase.listen({ port: 0 }, resolve)) 56 | 57 | await new Promise(resolve => target.listen({ port: 0 }, resolve)) 58 | 59 | const result = await request(`http://localhost:${instance.server.address().port}/test`) 60 | t.assert.strictEqual(result.statusCode, 200) 61 | 62 | const result1 = await request(`http://localhost:${instanceWithoutBase.server.address().port}/test2`) 63 | t.assert.strictEqual(result1.statusCode, 200) 64 | }) 65 | -------------------------------------------------------------------------------- /test/get-upstream-type.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const t = require('node:test') 4 | const Fastify = require('fastify') 5 | const { request } = require('undici') 6 | const From = require('..') 7 | const http = require('node:http') 8 | 9 | const instance = Fastify() 10 | instance.register(From, { 11 | disableCache: true 12 | }) 13 | 14 | t.test('getUpstream type', async (t) => { 15 | t.plan(5) 16 | t.after(() => instance.close()) 17 | 18 | const target = http.createServer((req, res) => { 19 | t.assert.ok('request proxied') 20 | t.assert.strictEqual(req.method, 'GET') 21 | res.end(req.headers.host) 22 | }) 23 | 24 | instance.get('/', (request, reply) => { 25 | reply.from(`http://localhost:${target.address().port}`, { 26 | getUpstream: (req) => { 27 | t.assert.ok('getUpstream called with correct request parameter') 28 | t.assert.strictEqual(req, request) 29 | return `http://localhost:${target.address().port}` 30 | } 31 | }) 32 | }) 33 | 34 | t.after(() => target.close()) 35 | 36 | await new Promise(resolve => instance.listen({ port: 0 }, resolve)) 37 | 38 | await new Promise(resolve => target.listen({ port: 0 }, resolve)) 39 | 40 | const result = await request(`http://localhost:${instance.server.address().port}`) 41 | 42 | t.assert.strictEqual(result.statusCode, 200) 43 | }) 44 | -------------------------------------------------------------------------------- /test/get-upstream-undici.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const t = require('node:test') 4 | const Fastify = require('fastify') 5 | const { request } = require('undici') 6 | const From = require('..') 7 | const http = require('node:http') 8 | 9 | const instance = Fastify() 10 | instance.register(From, { 11 | disableCache: true 12 | }) 13 | 14 | t.test('getUpstream undici', async (t) => { 15 | t.plan(4) 16 | t.after(() => instance.close()) 17 | 18 | const target = http.createServer((req, res) => { 19 | t.assert.ok('request proxied') 20 | t.assert.strictEqual(req.method, 'GET') 21 | res.end(req.headers.host) 22 | }) 23 | 24 | instance.get('/test', (_request, reply) => { 25 | reply.from('/test', { 26 | getUpstream: () => { 27 | t.assert.ok('getUpstream called') 28 | return `http://localhost:${target.address().port}` 29 | } 30 | }) 31 | }) 32 | 33 | t.after(() => target.close()) 34 | 35 | await new Promise(resolve => instance.listen({ port: 0 }, resolve)) 36 | 37 | await new Promise(resolve => target.listen({ port: 0 }, resolve)) 38 | 39 | const result = await request(`http://localhost:${instance.server.address().port}/test`) 40 | 41 | t.assert.strictEqual(result.statusCode, 200) 42 | }) 43 | -------------------------------------------------------------------------------- /test/host-header.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const Fastify = require('fastify') 5 | const { request, Agent } = require('undici') 6 | const From = require('..') 7 | const nock = require('nock') 8 | 9 | test('hostname', async (t) => { 10 | const instance = Fastify() 11 | t.after(() => instance.close()) 12 | 13 | nock('http://httpbin.org') 14 | .get('/ip') 15 | .reply(200, function () { 16 | t.assert.strictEqual(this.req.headers.host, 'httpbin.org') 17 | return { origin: '127.0.0.1' } 18 | }) 19 | 20 | instance.get('*', (_request, reply) => { 21 | reply.from(null, { 22 | rewriteRequestHeaders: (originalReq, headers) => { 23 | t.assert.strictEqual(headers.host, 'httpbin.org') 24 | t.assert.strictEqual(originalReq.headers.host, `localhost:${instance.server.address().port}`) 25 | return headers 26 | } 27 | }) 28 | }) 29 | 30 | instance.register(From, { 31 | base: 'http://httpbin.org', 32 | http: {} // force the use of Node.js core 33 | }) 34 | 35 | await instance.listen({ port: 0 }) 36 | 37 | const res = await request(`http://localhost:${instance.server.address().port}/ip`, { 38 | dispatcher: new Agent({ 39 | pipelining: 0 40 | }) 41 | }) 42 | t.assert.strictEqual(res.statusCode, 200) 43 | t.assert.strictEqual(res.headers['content-type'], 'application/json') 44 | t.assert.strictEqual(typeof (await res.body.json()).origin, 'string') 45 | }) 46 | 47 | test('hostname and port', async (t) => { 48 | const instance = Fastify() 49 | t.after(() => instance.close()) 50 | 51 | nock('http://httpbin.org:8080') 52 | .get('/ip') 53 | .reply(200, function () { 54 | t.assert.strictEqual(this.req.headers.host, 'httpbin.org:8080') 55 | return { origin: '127.0.0.1' } 56 | }) 57 | 58 | instance.register(From, { 59 | base: 'http://httpbin.org:8080', 60 | http: true 61 | }) 62 | 63 | instance.get('*', (_request, reply) => { 64 | reply.from() 65 | }) 66 | 67 | await instance.listen({ port: 0 }) 68 | 69 | const res = await request(`http://localhost:${instance.server.address().port}/ip`, { 70 | dispatcher: new Agent({ 71 | pipelining: 0 72 | }) 73 | }) 74 | t.assert.strictEqual(res.statusCode, 200) 75 | t.assert.strictEqual(res.headers['content-type'], 'application/json') 76 | t.assert.strictEqual(typeof (await res.body.json()).origin, 'string') 77 | }) 78 | -------------------------------------------------------------------------------- /test/http-agents.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const t = require('node:test') 4 | const Fastify = require('fastify') 5 | const { request } = require('undici') 6 | const From = require('..') 7 | const http = require('node:http') 8 | const https = require('node:https') 9 | 10 | const instance = Fastify() 11 | 12 | t.test('http agents', async (t) => { 13 | t.plan(7) 14 | t.after(() => instance.close()) 15 | 16 | const target = http.createServer((req, res) => { 17 | t.assert.ok('request proxied') 18 | t.assert.strictEqual(req.method, 'GET') 19 | t.assert.strictEqual(req.url, '/') 20 | res.statusCode = 205 21 | res.setHeader('Content-Type', 'text/plain') 22 | res.setHeader('x-my-header', 'hello!') 23 | res.end('hello world') 24 | }) 25 | 26 | instance.get('/', (_request, reply) => { 27 | reply.from() 28 | }) 29 | 30 | t.after(() => target.close()) 31 | 32 | await new Promise(resolve => target.listen({ port: 0 }, resolve)) 33 | 34 | instance.register(From, { 35 | base: `http://localhost:${target.address().port}`, 36 | http: { 37 | agents: { 38 | 'http:': new http.Agent({}), 39 | 'https:': new https.Agent({}) 40 | } 41 | } 42 | }) 43 | 44 | await new Promise(resolve => instance.listen({ port: 0 }, resolve)) 45 | 46 | const result = await request(`http://localhost:${instance.server.address().port}`) 47 | 48 | t.assert.strictEqual(result.headers['content-type'], 'text/plain') 49 | t.assert.strictEqual(result.headers['x-my-header'], 'hello!') 50 | t.assert.strictEqual(result.statusCode, 205) 51 | t.assert.strictEqual(await result.body.text(), 'hello world') 52 | }) 53 | -------------------------------------------------------------------------------- /test/http-global-agent.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const Fastify = require('fastify') 5 | const { request } = require('undici') 6 | const From = require('..') 7 | const http = require('node:http') 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.after(() => instance.close()) 15 | instance.get('/', (_request, reply) => { 16 | reply.from() 17 | }) 18 | 19 | const target = http.createServer((req, res) => { 20 | t.assert.ok('request proxied') 21 | t.assert.strictEqual(req.method, 'GET') 22 | t.assert.strictEqual(req.url, '/') 23 | res.statusCode = 200 24 | res.end() 25 | }) 26 | t.after(() => target.close()) 27 | 28 | await new Promise(resolve => target.listen({ port: 0 }, resolve)) 29 | 30 | instance.register(From, { 31 | base: `http://localhost:${target.address().port}`, 32 | globalAgent: true, 33 | http: { 34 | } 35 | }) 36 | 37 | await new Promise(resolve => instance.listen({ port: 0 }, resolve)) 38 | 39 | const result = await request(`http://localhost:${instance.server.address().port}`) 40 | 41 | t.assert.strictEqual(result.statusCode, 200) 42 | 43 | target.close() 44 | }) 45 | -------------------------------------------------------------------------------- /test/http-http2.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const Fastify = require('fastify') 5 | const { request, Agent } = require('undici') 6 | const From = require('..') 7 | 8 | test('http -> http2', async (t) => { 9 | const instance = Fastify() 10 | 11 | t.after(() => instance.close()) 12 | 13 | const target = Fastify({ 14 | http2: true 15 | }) 16 | 17 | target.get('/', (_request, reply) => { 18 | t.assert.ok('request proxied') 19 | reply.code(404).header('x-my-header', 'hello!').send({ 20 | hello: 'world' 21 | }) 22 | }) 23 | 24 | instance.get('/', (_request, reply) => { 25 | reply.from() 26 | }) 27 | 28 | t.after(() => target.close()) 29 | 30 | await target.listen({ port: 0 }) 31 | 32 | instance.register(From, { 33 | base: `http://localhost:${target.server.address().port}`, 34 | http2: true 35 | }) 36 | 37 | await instance.listen({ port: 0 }) 38 | 39 | const result = await request(`http://localhost:${instance.server.address().port}`, { dispatcher: new Agent({ pipelining: 0 }) }) 40 | 41 | t.assert.strictEqual(result.statusCode, 404) 42 | t.assert.strictEqual(result.headers['x-my-header'], 'hello!') 43 | t.assert.match(result.headers['content-type'], /application\/json/) 44 | t.assert.deepStrictEqual(await result.body.json(), { hello: 'world' }) 45 | instance.close() 46 | target.close() 47 | }) 48 | -------------------------------------------------------------------------------- /test/http-invalid-target.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const Fastify = require('fastify') 5 | const { request } = require('undici') 6 | const From = require('..') 7 | 8 | test('http invalid target', async (t) => { 9 | const instance = Fastify() 10 | 11 | t.after(() => instance.close()) 12 | 13 | instance.get('/', (_request, reply) => { 14 | reply.from() 15 | }) 16 | instance.register(From, { 17 | base: 'http://abc.xyz1' 18 | }) 19 | 20 | await instance.listen({ port: 0 }) 21 | 22 | const result = await request(`http://localhost:${instance.server.address().port}`) 23 | 24 | t.assert.strictEqual(result.statusCode, 503) 25 | t.assert.match(result.headers['content-type'], /application\/json/) 26 | t.assert.deepStrictEqual(await result.body.json(), { 27 | statusCode: 503, 28 | code: 'FST_REPLY_FROM_SERVICE_UNAVAILABLE', 29 | error: 'Service Unavailable', 30 | message: 'Service Unavailable' 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /test/http-retry.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Fastify = require('fastify') 4 | const { request, Agent } = require('undici') 5 | const From = require('..') 6 | const { test } = require('node:test') 7 | 8 | let retryNum = 1 9 | 10 | const target = require('node:http').createServer(function (req, res) { 11 | if (retryNum % 2 !== 0) { 12 | req.socket.destroy() 13 | } else { 14 | res.statusCode = 200 15 | res.setHeader('Content-Type', 'text/plain') 16 | res.end('hello world') 17 | } 18 | 19 | retryNum += 1 20 | }) 21 | 22 | test('Will retry', async function (t) { 23 | t.after(() => { retryNum = 1 }) 24 | 25 | await target.listen({ port: 0 }) 26 | t.after(() => target.close()) 27 | 28 | const instance = Fastify() 29 | 30 | instance.register(From, { http: true }) 31 | 32 | instance.get('/', (_request, reply) => { 33 | reply.from(`http://localhost:${target.address().port}/`, { 34 | retriesCount: 1, 35 | onError: (reply, { error }) => { 36 | t.assert.strictEqual(error.code, 'ECONNRESET') 37 | reply.send(error) 38 | } 39 | }) 40 | }) 41 | 42 | await instance.listen({ port: 0 }) 43 | t.after(() => instance.close()) 44 | 45 | const { statusCode } = await request(`http://localhost:${instance.server.address().port}/`, { dispatcher: new Agent({ pipelining: 0 }) }) 46 | t.assert.strictEqual(statusCode, 200) 47 | }) 48 | 49 | test('will not retry', async function (t) { 50 | t.after(() => { retryNum = 1 }) 51 | 52 | await target.listen({ port: 0 }) 53 | t.after(() => target.close()) 54 | 55 | const instance = Fastify() 56 | 57 | instance.register(From, { http: true }) 58 | 59 | instance.get('/', (_request, reply) => { 60 | reply.from(`http://localhost:${target.address().port}/`, { 61 | retriesCount: 0, 62 | onError: (reply, { error }) => { 63 | t.assert.strictEqual(error.code, 'ECONNRESET') 64 | reply.send(error) 65 | } 66 | }) 67 | }) 68 | 69 | await instance.listen({ port: 0 }) 70 | t.after(() => instance.close()) 71 | 72 | const result = await request(`http://localhost:${instance.server.address().port}/`, { dispatcher: new Agent({ pipelining: 0 }) }) 73 | 74 | t.assert.strictEqual(result.statusCode, 500) 75 | }) 76 | 77 | test('will not retry unsupported method', async function (t) { 78 | t.after(() => { retryNum = 1 }) 79 | 80 | await new Promise(resolve => target.listen({ port: 0 }, resolve)) 81 | t.after(() => target.close()) 82 | 83 | const instance = Fastify() 84 | 85 | instance.register(From, { http: true, retryMethods: ['DELETE'] }) 86 | 87 | instance.get('/', (_request, reply) => { 88 | reply.from(`http://localhost:${target.address().port}/`, { 89 | retriesCount: 1, 90 | onError: (reply, { error }) => { 91 | t.assert.strictEqual(error.code, 'ECONNRESET') 92 | reply.send(error) 93 | } 94 | }) 95 | }) 96 | 97 | await instance.listen({ port: 0 }) 98 | t.after(() => instance.close()) 99 | 100 | const result = await request(`http://localhost:${instance.server.address().port}/`, { dispatcher: new Agent({ pipelining: 0 }) }) 101 | t.assert.strictEqual(result.statusCode, 500) 102 | }) 103 | -------------------------------------------------------------------------------- /test/http-timeout.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const Fastify = require('fastify') 5 | const { request, Agent } = require('undici') 6 | const From = require('..') 7 | const FakeTimers = require('@sinonjs/fake-timers') 8 | 9 | test('http request timeout', async (t) => { 10 | const clock = FakeTimers.createClock() 11 | const target = Fastify() 12 | t.after(() => target.close()) 13 | 14 | target.get('/', (_request, reply) => { 15 | t.assert.ok('request arrives') 16 | 17 | setTimeout(() => { 18 | reply.status(200).send('hello world') 19 | }, 200) 20 | 21 | clock.tick(200) 22 | }) 23 | 24 | await target.listen({ port: 0 }) 25 | 26 | const instance = Fastify() 27 | t.after(() => instance.close()) 28 | 29 | instance.register(From, { http: { requestOptions: { timeout: 100 } } }) 30 | 31 | instance.get('/', (_request, reply) => { 32 | reply.from(`http://localhost:${target.server.address().port}/`) 33 | }) 34 | 35 | await instance.listen({ port: 0 }) 36 | 37 | const result = await request(`http://localhost:${instance.server.address().port}/`, { 38 | dispatcher: new Agent({ 39 | pipelining: 0 40 | }) 41 | }) 42 | 43 | t.assert.strictEqual(result.statusCode, 504) 44 | t.assert.match(result.headers['content-type'], /application\/json/) 45 | t.assert.deepStrictEqual(await result.body.json(), { 46 | statusCode: 504, 47 | code: 'FST_REPLY_FROM_GATEWAY_TIMEOUT', 48 | error: 'Gateway Timeout', 49 | message: 'Gateway Timeout' 50 | }) 51 | clock.tick(200) 52 | }) 53 | 54 | test('http request with specific timeout', async (t) => { 55 | const clock = FakeTimers.createClock() 56 | const target = Fastify() 57 | t.after(() => target.close()) 58 | 59 | target.get('/', (_request, reply) => { 60 | t.assert.ok('request arrives') 61 | 62 | setTimeout(() => { 63 | reply.status(200).send('hello world') 64 | }, 200) 65 | 66 | clock.tick(200) 67 | }) 68 | 69 | await target.listen({ port: 0 }) 70 | 71 | const instance = Fastify() 72 | t.after(() => instance.close()) 73 | 74 | instance.register(From, { http: { requestOptions: { timeout: 100 } } }) 75 | 76 | instance.get('/success', (_request, reply) => { 77 | reply.from(`http://localhost:${target.server.address().port}/`, { 78 | timeout: 300 79 | }) 80 | }) 81 | instance.get('/fail', (_request, reply) => { 82 | reply.from(`http://localhost:${target.server.address().port}/`, { 83 | timeout: 50 84 | }) 85 | }) 86 | 87 | await instance.listen({ port: 0 }) 88 | const result = await request(`http://localhost:${instance.server.address().port}/success`, { 89 | dispatcher: new Agent({ 90 | pipelining: 0 91 | }) 92 | }) 93 | t.assert.strictEqual(result.statusCode, 200) 94 | 95 | const result2 = await request(`http://localhost:${instance.server.address().port}/fail`, { 96 | dispatcher: new Agent({ 97 | pipelining: 0 98 | }) 99 | }) 100 | 101 | t.assert.strictEqual(result2.statusCode, 504) 102 | t.assert.match(result2.headers['content-type'], /application\/json/) 103 | t.assert.deepStrictEqual(await result2.body.json(), { 104 | statusCode: 504, 105 | code: 'FST_REPLY_FROM_GATEWAY_TIMEOUT', 106 | error: 'Gateway Timeout', 107 | message: 'Gateway Timeout' 108 | }) 109 | }) 110 | 111 | test('http sse removes timeout test', async (t) => { 112 | const target = Fastify() 113 | t.after(() => target.close()) 114 | 115 | target.get('/', (_request, reply) => { 116 | t.assert.ok('request arrives') 117 | 118 | reply.header('content-type', 'text/event-stream').status(200).send('hello world') 119 | }) 120 | 121 | await target.listen({ port: 0 }) 122 | 123 | const instance = Fastify() 124 | t.after(() => instance.close()) 125 | 126 | instance.register(From, { http: { requestOptions: { timeout: 100 } } }) 127 | 128 | instance.get('/', (_request, reply) => { 129 | reply.from(`http://localhost:${target.server.address().port}/`) 130 | }) 131 | 132 | await instance.listen({ port: 0 }) 133 | 134 | const { statusCode } = await request(`http://localhost:${instance.server.address().port}/`, { 135 | dispatcher: new Agent({ 136 | pipelining: 0 137 | }) 138 | }) 139 | t.assert.strictEqual(statusCode, 200) 140 | }) 141 | -------------------------------------------------------------------------------- /test/http2-http2.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const h2url = require('h2url') 4 | const t = require('node:test') 5 | const Fastify = require('fastify') 6 | const From = require('..') 7 | const fs = require('node:fs') 8 | const path = require('node:path') 9 | const certs = { 10 | key: fs.readFileSync(path.join(__dirname, 'fixtures', 'fastify.key')), 11 | cert: fs.readFileSync(path.join(__dirname, 'fixtures', 'fastify.cert')) 12 | } 13 | 14 | t.test('http2 -> http2', async (t) => { 15 | const instance = Fastify({ 16 | http2: true, 17 | https: certs 18 | }) 19 | 20 | t.after(() => instance.close()) 21 | 22 | const target = Fastify({ 23 | http2: true 24 | }) 25 | 26 | target.get('/', (_request, reply) => { 27 | t.assert.ok('request proxied') 28 | reply.code(404).header('x-my-header', 'hello!').send({ 29 | hello: 'world' 30 | }) 31 | }) 32 | 33 | instance.get('/', (_request, reply) => { 34 | reply.from() 35 | }) 36 | 37 | t.after(() => target.close()) 38 | 39 | await target.listen({ port: 0 }) 40 | 41 | instance.register(From, { 42 | base: `http://localhost:${target.server.address().port}`, 43 | http2: true, 44 | rejectUnauthorized: false 45 | }) 46 | 47 | await instance.listen({ port: 0 }) 48 | 49 | const { headers, body } = await h2url.concat({ 50 | url: `https://localhost:${instance.server.address().port}` 51 | }) 52 | 53 | t.assert.strictEqual(headers[':status'], 404) 54 | t.assert.strictEqual(headers['x-my-header'], 'hello!') 55 | t.assert.match(headers['content-type'], /application\/json/) 56 | t.assert.deepStrictEqual(JSON.parse(body), { hello: 'world' }) 57 | instance.close() 58 | target.close() 59 | }) 60 | -------------------------------------------------------------------------------- /test/http2-https.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const h2url = require('h2url') 4 | const t = require('node:test') 5 | const assert = require('node:assert') 6 | const Fastify = require('fastify') 7 | const { request, Agent } = require('undici') 8 | const From = require('..') 9 | const fs = require('node:fs') 10 | const path = require('node:path') 11 | const certs = { 12 | allowHTTP1: true, // fallback support for HTTP1 13 | key: fs.readFileSync(path.join(__dirname, 'fixtures', 'fastify.key')), 14 | cert: fs.readFileSync(path.join(__dirname, 'fixtures', 'fastify.cert')) 15 | } 16 | 17 | const instance = Fastify({ 18 | http2: true, 19 | https: certs 20 | }) 21 | 22 | const target = Fastify({ 23 | https: certs 24 | }) 25 | 26 | target.get('/', (_request, reply) => { 27 | assert.ok('request proxied') 28 | reply.code(404).header('x-my-header', 'hello!').send({ 29 | hello: 'world' 30 | }) 31 | }) 32 | 33 | instance.get('/', (_request, reply) => { 34 | reply.from() 35 | }) 36 | 37 | async function run (t) { 38 | await target.listen({ port: 0 }) 39 | 40 | instance.register(From, { 41 | base: `https://localhost:${target.server.address().port}`, 42 | rejectUnauthorized: false 43 | }) 44 | 45 | await instance.listen({ port: 0 }) 46 | 47 | await t.test('http2 -> https', async (t) => { 48 | t.plan(4) 49 | const { headers, body } = await h2url.concat({ 50 | url: `https://localhost:${instance.server.address().port}` 51 | }) 52 | 53 | t.assert.strictEqual(headers[':status'], 404) 54 | t.assert.strictEqual(headers['x-my-header'], 'hello!') 55 | t.assert.match(headers['content-type'], /application\/json/) 56 | t.assert.deepStrictEqual(JSON.parse(body), { hello: 'world' }) 57 | }) 58 | 59 | await t.test('https -> https', async (t) => { 60 | t.plan(4) 61 | const result = await request(`https://localhost:${instance.server.address().port}`, { 62 | dispatcher: new Agent({ 63 | connect: { 64 | rejectUnauthorized: false 65 | } 66 | }) 67 | }) 68 | 69 | t.assert.strictEqual(result.statusCode, 404) 70 | t.assert.strictEqual(result.headers['x-my-header'], 'hello!') 71 | t.assert.match(result.headers['content-type'], /application\/json/) 72 | t.assert.deepStrictEqual(await result.body.json(), { hello: 'world' }) 73 | }) 74 | } 75 | 76 | t.test('http2 -> https', async (t) => { 77 | t.plan(2) 78 | t.after(() => instance.close()) 79 | t.after(() => target.close()) 80 | 81 | await run(t) 82 | }) 83 | -------------------------------------------------------------------------------- /test/http2-invalid-base.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const Fastify = require('fastify') 5 | const From = require('../index') 6 | 7 | test('http2 invalid base', async (t) => { 8 | const instance = Fastify() 9 | 10 | await t.assert.rejects(async () => instance.register(From, { 11 | http2: { requestTimeout: 100 } 12 | }), new Error('Option base is required when http2 is true')) 13 | }) 14 | -------------------------------------------------------------------------------- /test/http2-invalid-target.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const Fastify = require('fastify') 5 | const { request } = require('undici') 6 | const From = require('..') 7 | 8 | test('http2 invalid target', async (t) => { 9 | const instance = Fastify() 10 | 11 | t.after(() => instance.close()) 12 | 13 | instance.get('/', (_request, reply) => { 14 | reply.from() 15 | }) 16 | instance.register(From, { 17 | base: 'http://abc.xyz1', 18 | http2: true 19 | }) 20 | 21 | await instance.listen({ port: 0 }) 22 | 23 | const result = await request(`http://localhost:${instance.server.address().port}`) 24 | 25 | t.assert.strictEqual(result.statusCode, 503) 26 | t.assert.match(result.headers['content-type'], /application\/json/) 27 | t.assert.deepStrictEqual(await result.body.json(), { 28 | statusCode: 503, 29 | code: 'FST_REPLY_FROM_SERVICE_UNAVAILABLE', 30 | error: 'Service Unavailable', 31 | message: 'Service Unavailable' 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /test/http2-target-crash.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const Fastify = require('fastify') 5 | const { request } = require('undici') 6 | const From = require('..') 7 | 8 | test('http -> http2 crash', async (t) => { 9 | const instance = Fastify() 10 | 11 | t.after(() => instance.close()) 12 | 13 | const target = Fastify({ 14 | http2: true 15 | }) 16 | 17 | target.get('/', (_request, reply) => { 18 | t.assert.ok('request proxied') 19 | reply.code(200).send({ 20 | hello: 'world' 21 | }) 22 | }) 23 | 24 | instance.get('/', (_request, reply) => { 25 | reply.from() 26 | }) 27 | 28 | t.after(() => target.close()) 29 | 30 | await target.listen({ port: 0 }) 31 | 32 | instance.register(From, { 33 | base: `http://localhost:${target.server.address().port}`, 34 | http2: true 35 | }) 36 | 37 | await instance.listen({ port: 0 }) 38 | 39 | await target.close() 40 | const result = await request(`http://localhost:${instance.server.address().port}`) 41 | 42 | t.assert.strictEqual(result.statusCode, 503) 43 | t.assert.match(result.headers['content-type'], /application\/json/) 44 | t.assert.deepStrictEqual(await result.body.json(), { 45 | statusCode: 503, 46 | code: 'FST_REPLY_FROM_SERVICE_UNAVAILABLE', 47 | error: 'Service Unavailable', 48 | message: 'Service Unavailable' 49 | }) 50 | }) 51 | -------------------------------------------------------------------------------- /test/http2-target-multi-crash.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const Fastify = require('fastify') 5 | const { request } = require('undici') 6 | const From = require('..') 7 | 8 | test('http -> http2 crash multiple times', async (t) => { 9 | const instance = Fastify() 10 | 11 | t.after(() => instance.close()) 12 | 13 | instance.get('/', (_request, reply) => { 14 | reply.from() 15 | }) 16 | 17 | instance.register(From, { 18 | base: 'http://localhost:3128', 19 | http2: { 20 | sessionTimeout: 6000 21 | }, 22 | sessionTimeout: 200 23 | }) 24 | 25 | await instance.listen({ port: 0 }) 26 | let target = setupTarget() 27 | await target.listen({ port: 3128 }) 28 | await request(`http://localhost:${instance.server.address().port}`) 29 | await target.close() 30 | target = setupTarget() 31 | await target.listen({ port: 3128 }) 32 | await request(`http://localhost:${instance.server.address().port}`) 33 | await target.close() 34 | const result = await request(`http://localhost:${instance.server.address().port}`) 35 | 36 | t.assert.strictEqual(result.statusCode, 503) 37 | t.assert.match(result.headers['content-type'], /application\/json/) 38 | t.assert.deepStrictEqual(await result.body.json(), { 39 | statusCode: 503, 40 | code: 'FST_REPLY_FROM_SERVICE_UNAVAILABLE', 41 | error: 'Service Unavailable', 42 | message: 'Service Unavailable' 43 | }) 44 | 45 | function setupTarget () { 46 | const target = Fastify({ 47 | http2: true 48 | }) 49 | 50 | target.get('/', (request, reply) => { 51 | t.assert.ok('request proxied') 52 | reply.code(200).send({ 53 | hello: 'world' 54 | }) 55 | }) 56 | return target 57 | } 58 | }) 59 | -------------------------------------------------------------------------------- /test/http2-timeout-disabled.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const Fastify = require('fastify') 5 | const { request, Agent } = require('undici') 6 | const From = require('..') 7 | 8 | test('http2 request timeout disabled', async (t) => { 9 | const target = Fastify({ http2: true }) 10 | t.after(() => target.close()) 11 | 12 | target.get('/', () => { 13 | t.assert.ok('request arrives') 14 | }) 15 | 16 | await target.listen({ port: 0 }) 17 | 18 | const instance = Fastify() 19 | t.after(() => instance.close()) 20 | 21 | instance.register(From, { 22 | base: `http://localhost:${target.server.address().port}`, 23 | http2: { requestTimeout: 0, sessionTimeout: 16000 } 24 | }) 25 | 26 | instance.get('/', (_request, reply) => { 27 | reply.from(`http://localhost:${target.server.address().port}/`) 28 | }) 29 | 30 | await instance.listen({ port: 0 }) 31 | 32 | const result = await Promise.race([ 33 | request(`http://localhost:${instance.server.address().port}/`, { 34 | dispatcher: new Agent({ 35 | pipelining: 0 36 | }) 37 | }), 38 | new Promise(resolve => setTimeout(resolve, 11000, 'passed')) 39 | ]) 40 | 41 | // if we wait 11000 ms without a timeout error, we assume disabling the timeout worked 42 | // 10000 ms is the default timeout 43 | t.assert.strictEqual(result, 'passed') 44 | }) 45 | 46 | test('http2 session timeout disabled', async (t) => { 47 | const target = Fastify({ http2: true }) 48 | 49 | target.get('/', () => { 50 | t.assert.ok('request arrives') 51 | }) 52 | 53 | await target.listen({ port: 0 }) 54 | 55 | const instance = Fastify() 56 | 57 | instance.register(From, { 58 | sessionTimeout: 3000, 59 | destroyAgent: true, 60 | base: `http://localhost:${target.server.address().port}`, 61 | http2: { requestTimeout: 0, sessionTimeout: 0 } 62 | }) 63 | 64 | instance.get('/', (_request, reply) => { 65 | reply.from(`http://localhost:${target.server.address().port}/`) 66 | }) 67 | 68 | await instance.listen({ port: 0 }) 69 | 70 | const abortController = new AbortController() 71 | 72 | const result = await Promise.race([ 73 | request(`http://localhost:${instance.server.address().port}/`, { 74 | dispatcher: new Agent({ 75 | pipelining: 0 76 | }), 77 | signal: abortController.signal 78 | }), 79 | new Promise(resolve => setTimeout(resolve, 4000, 'passed')) 80 | ]) 81 | 82 | // clean up right after the timeout, otherwise test will hang 83 | abortController.abort() 84 | target.close() 85 | instance.close() 86 | 87 | // if we wait 4000 ms without a timeout error, we assume disabling the session timeout for reply-from worked 88 | // because we pass 3000 ms as session timeout to the Fastify options itself 89 | t.assert.strictEqual(result, 'passed') 90 | }) 91 | -------------------------------------------------------------------------------- /test/http2-timeout.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const Fastify = require('fastify') 5 | const { request, Agent } = require('undici') 6 | const From = require('..') 7 | const FakeTimers = require('@sinonjs/fake-timers') 8 | 9 | test('http2 request timeout', async (t) => { 10 | const target = Fastify({ http2: true, sessionTimeout: 0 }) 11 | t.after(() => target.close()) 12 | 13 | target.get('/', () => { 14 | t.assert.ok('request arrives') 15 | }) 16 | 17 | await target.listen({ port: 0 }) 18 | 19 | const instance = Fastify() 20 | t.after(() => instance.close()) 21 | 22 | instance.register(From, { 23 | base: `http://localhost:${target.server.address().port}`, 24 | http2: { requestTimeout: 100, sessionTimeout: 6000 } 25 | }) 26 | 27 | instance.get('/', (_request, reply) => { 28 | reply.from(`http://localhost:${target.server.address().port}/`) 29 | }) 30 | 31 | await instance.listen({ port: 0 }) 32 | 33 | const result = await request(`http://localhost:${instance.server.address().port}/`, { 34 | dispatcher: new Agent({ 35 | pipelining: 0 36 | }) 37 | }) 38 | t.assert.strictEqual(result.statusCode, 504) 39 | t.assert.match(result.headers['content-type'], /application\/json/) 40 | t.assert.deepStrictEqual(await result.body.json(), { 41 | statusCode: 504, 42 | code: 'FST_REPLY_FROM_GATEWAY_TIMEOUT', 43 | error: 'Gateway Timeout', 44 | message: 'Gateway Timeout' 45 | }) 46 | }) 47 | 48 | test('http2 request with specific timeout', async (t) => { 49 | const clock = FakeTimers.createClock() 50 | const target = Fastify({ http2: true }) 51 | t.after(() => target.close()) 52 | 53 | target.get('/', (_request, reply) => { 54 | t.assert.ok('request arrives') 55 | 56 | setTimeout(() => { 57 | reply.status(200).send('hello world') 58 | }, 200) 59 | 60 | clock.tick(200) 61 | }) 62 | 63 | await target.listen({ port: 0 }) 64 | 65 | const instance = Fastify() 66 | t.after(() => instance.close()) 67 | 68 | instance.register(From, { 69 | base: `http://localhost:${target.server.address().port}`, 70 | http2: { requestTimeout: 100, sessionTimeout: 6000 } 71 | }) 72 | 73 | instance.get('/success', (_request, reply) => { 74 | reply.from(`http://localhost:${target.server.address().port}/`, { 75 | timeout: 300 76 | }) 77 | }) 78 | instance.get('/fail', (_request, reply) => { 79 | reply.from(`http://localhost:${target.server.address().port}/`, { 80 | timeout: 50 81 | }) 82 | }) 83 | 84 | await instance.listen({ port: 0 }) 85 | const result = await request(`http://localhost:${instance.server.address().port}/success`, { 86 | dispatcher: new Agent({ 87 | pipelining: 0 88 | }) 89 | }) 90 | t.assert.strictEqual(result.statusCode, 200) 91 | 92 | const result2 = await request(`http://localhost:${instance.server.address().port}/fail`, { 93 | dispatcher: new Agent({ 94 | pipelining: 0 95 | }) 96 | }) 97 | t.assert.strictEqual(result2.statusCode, 504) 98 | t.assert.match(result2.headers['content-type'], /application\/json/) 99 | t.assert.deepStrictEqual(await result2.body.json(), { 100 | statusCode: 504, 101 | code: 'FST_REPLY_FROM_GATEWAY_TIMEOUT', 102 | error: 'Gateway Timeout', 103 | message: 'Gateway Timeout' 104 | }) 105 | }) 106 | 107 | test('http2 session timeout', async (t) => { 108 | const target = Fastify({ http2: true, sessionTimeout: 0 }) 109 | t.after(() => target.close()) 110 | 111 | target.get('/', () => { 112 | t.assert.ok('request arrives') 113 | }) 114 | 115 | await target.listen({ port: 0 }) 116 | 117 | const instance = Fastify() 118 | t.after(() => instance.close()) 119 | 120 | instance.register(From, { 121 | base: `http://localhost:${target.server.address().port}`, 122 | http2: { sessionTimeout: 100 } 123 | }) 124 | 125 | instance.get('/', (_request, reply) => { 126 | reply.from(`http://localhost:${target.server.address().port}/`) 127 | }) 128 | 129 | await instance.listen({ port: 0 }) 130 | 131 | const result = await request(`http://localhost:${instance.server.address().port}/`, { 132 | dispatcher: new Agent({ 133 | pipelining: 0 134 | }) 135 | }) 136 | 137 | t.assert.strictEqual(result.statusCode, 504) 138 | t.assert.match(result.headers['content-type'], /application\/json/) 139 | t.assert.deepStrictEqual(await result.body.json(), { 140 | statusCode: 504, 141 | code: 'FST_REPLY_FROM_GATEWAY_TIMEOUT', 142 | error: 'Gateway Timeout', 143 | message: 'Gateway Timeout' 144 | }) 145 | }) 146 | 147 | test('http2 sse removes request and session timeout test', async (t) => { 148 | const target = Fastify({ http2: true, sessionTimeout: 0 }) 149 | 150 | target.get('/', (_request, reply) => { 151 | t.assert.ok('request arrives') 152 | 153 | reply.status(200).header('content-type', 'text/event-stream').send('hello world') 154 | }) 155 | 156 | await target.listen({ port: 0 }) 157 | 158 | const instance = Fastify() 159 | 160 | instance.register(From, { 161 | base: `http://localhost:${target.server.address().port}`, 162 | http2: { sessionTimeout: 100 } 163 | }) 164 | 165 | instance.get('/', (_request, reply) => { 166 | reply.from(`http://localhost:${target.server.address().port}/`) 167 | }) 168 | 169 | await instance.listen({ port: 0 }) 170 | 171 | t.after(() => instance.close()) 172 | t.after(() => target.close()) 173 | 174 | const { statusCode } = await request(`http://localhost:${instance.server.address().port}/`, { dispatcher: new Agent({ pipelining: 0 }) }) 175 | t.assert.strictEqual(statusCode, 200) 176 | instance.close() 177 | target.close() 178 | }) 179 | -------------------------------------------------------------------------------- /test/http2-unix-socket.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const Fastify = require('fastify') 5 | const From = require('../index') 6 | 7 | test('throw an error if http2 is used with a Unix socket destination', async t => { 8 | t.plan(1) 9 | 10 | const instance = Fastify() 11 | 12 | await t.assert.rejects(async () => instance.register(From, { 13 | base: 'unix+http://localhost:1337', 14 | http2: { requestTimeout: 100 } 15 | }), new Error('Unix socket destination is not supported when http2 is true')) 16 | }) 17 | -------------------------------------------------------------------------------- /test/https-agents.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const t = require('node:test') 4 | const Fastify = require('fastify') 5 | const { request } = require('undici') 6 | const From = require('..') 7 | const http = require('node:http') 8 | const https = require('node:https') 9 | const { Agent } = require('undici') 10 | 11 | const fs = require('node:fs') 12 | const path = require('node:path') 13 | const certs = { 14 | key: fs.readFileSync(path.join(__dirname, 'fixtures', 'fastify.key')), 15 | cert: fs.readFileSync(path.join(__dirname, 'fixtures', 'fastify.cert')) 16 | } 17 | 18 | const instance = Fastify({ 19 | https: certs 20 | }) 21 | 22 | t.test('https agents', async (t) => { 23 | t.plan(7) 24 | t.after(() => instance.close()) 25 | 26 | const target = https.createServer(certs, (req, res) => { 27 | t.assert.ok('request proxied') 28 | t.assert.strictEqual(req.method, 'GET') 29 | t.assert.strictEqual(req.url, '/') 30 | res.statusCode = 205 31 | res.setHeader('Content-Type', 'text/plain') 32 | res.setHeader('x-my-header', 'hello!') 33 | res.end('hello world') 34 | }) 35 | 36 | instance.get('/', (_request, reply) => { 37 | reply.from() 38 | }) 39 | 40 | t.after(() => target.close()) 41 | 42 | await new Promise(resolve => target.listen({ port: 0 }, resolve)) 43 | 44 | instance.register(From, { 45 | base: `https://localhost:${target.address().port}`, 46 | http: { 47 | agents: { 48 | 'http:': new http.Agent({}), 49 | 'https:': new https.Agent({}) 50 | } 51 | } 52 | }) 53 | 54 | await new Promise(resolve => instance.listen({ port: 0 }, resolve)) 55 | 56 | const result = await request(`https://localhost:${instance.server.address().port}`, { 57 | dispatcher: new Agent({ 58 | connect: { 59 | rejectUnauthorized: false 60 | } 61 | }) 62 | }) 63 | 64 | t.assert.strictEqual(result.headers['content-type'], 'text/plain') 65 | t.assert.strictEqual(result.headers['x-my-header'], 'hello!') 66 | t.assert.strictEqual(result.statusCode, 205) 67 | t.assert.strictEqual(await result.body.text(), 'hello world') 68 | }) 69 | -------------------------------------------------------------------------------- /test/https-global-agent.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const Fastify = require('fastify') 5 | const { request } = require('undici') 6 | const From = require('..') 7 | const https = require('node:https') 8 | const { Agent } = require('undici') 9 | 10 | const fs = require('node:fs') 11 | const path = require('node:path') 12 | const certs = { 13 | key: fs.readFileSync(path.join(__dirname, 'fixtures', 'fastify.key')), 14 | cert: fs.readFileSync(path.join(__dirname, 'fixtures', 'fastify.cert')) 15 | } 16 | 17 | test('https global agent is used, but not destroyed', async (t) => { 18 | https.globalAgent.destroy = () => { 19 | t.fail() 20 | } 21 | const instance = Fastify({ 22 | https: certs 23 | }) 24 | t.after(() => instance.close()) 25 | instance.get('/', (_request, reply) => { 26 | reply.from() 27 | }) 28 | 29 | const target = https.createServer(certs, (req, res) => { 30 | t.assert.ok('request proxied') 31 | t.assert.strictEqual(req.method, 'GET') 32 | t.assert.strictEqual(req.url, '/') 33 | res.statusCode = 200 34 | res.end() 35 | }) 36 | t.after(() => target.close()) 37 | 38 | await new Promise(resolve => target.listen({ port: 0 }, resolve)) 39 | 40 | instance.register(From, { 41 | base: `https://localhost:${target.address().port}`, 42 | globalAgent: true, 43 | http: { 44 | } 45 | }) 46 | 47 | await new Promise(resolve => instance.listen({ port: 0 }, resolve)) 48 | 49 | const result = await request(`https://localhost:${instance.server.address().port}`, { 50 | dispatcher: new Agent({ 51 | connect: { 52 | rejectUnauthorized: false 53 | } 54 | }) 55 | }) 56 | 57 | t.assert.strictEqual(result.statusCode, 200) 58 | 59 | target.close() 60 | }) 61 | -------------------------------------------------------------------------------- /test/method.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const t = require('node:test') 4 | const Fastify = require('fastify') 5 | const { request } = require('undici') 6 | const From = require('..') 7 | const http = require('node:http') 8 | 9 | const instance = Fastify() 10 | 11 | t.test('method', async (t) => { 12 | t.plan(6) 13 | t.after(() => instance.close()) 14 | 15 | const bodyString = JSON.stringify({ hello: 'world' }) 16 | 17 | const parsedLength = Buffer.byteLength(bodyString) 18 | 19 | const target = http.createServer((req, res) => { 20 | t.assert.ok('request proxied') 21 | t.assert.deepStrictEqual(req.method, 'POST') 22 | t.assert.deepStrictEqual(req.headers['content-type'], 'application/json') 23 | t.assert.deepStrictEqual(req.headers['content-length'], `${parsedLength}`) 24 | let data = '' 25 | req.setEncoding('utf8') 26 | req.on('data', (d) => { 27 | data += d 28 | }) 29 | req.on('end', () => { 30 | t.assert.deepStrictEqual(JSON.parse(data), { hello: 'world' }) 31 | res.statusCode = 200 32 | res.setHeader('content-type', 'application/json') 33 | res.end(JSON.stringify({ something: 'else' })) 34 | }) 35 | }) 36 | 37 | instance.patch('/', (_request, reply) => { 38 | reply.from(`http://localhost:${target.address().port}`, { method: 'POST' }) 39 | }) 40 | 41 | t.after(() => target.close()) 42 | 43 | await new Promise(resolve => target.listen({ port: 0 }, resolve)) 44 | 45 | instance.addContentTypeParser('application/json', function (_req, payload, done) { 46 | done(null, payload) 47 | }) 48 | 49 | instance.register(From, { 50 | base: `http://localhost:${target.address().port}`, 51 | undici: true 52 | }) 53 | 54 | await new Promise(resolve => instance.listen({ port: 0 }, resolve)) 55 | 56 | const result = await request(`http://localhost:${instance.server.address().port}`, { 57 | method: 'PATCH', 58 | headers: { 59 | 'content-type': 'application/json' 60 | }, 61 | body: bodyString 62 | }) 63 | 64 | t.assert.deepStrictEqual(await result.body.json(), { something: 'else' }) 65 | }) 66 | -------------------------------------------------------------------------------- /test/modifyCoreObjects-false.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const t = require('node:test') 4 | const Fastify = require('fastify') 5 | const { request } = require('undici') 6 | const From = require('..') 7 | const http = require('node:http') 8 | 9 | const instance = Fastify({ modifyCoreObjects: false }) 10 | 11 | t.test('modifyCoreObjects false', async (t) => { 12 | t.plan(7) 13 | t.after(() => instance.close()) 14 | 15 | const target = http.createServer((req, res) => { 16 | t.assert.ok('request proxied') 17 | t.assert.strictEqual(req.method, 'GET') 18 | t.assert.strictEqual(req.url, '/') 19 | res.statusCode = 205 20 | res.setHeader('Content-Type', 'text/plain') 21 | res.setHeader('x-my-header', 'hello!') 22 | res.end('hello world') 23 | }) 24 | 25 | instance.get('/', (_request, reply) => { 26 | reply.from() 27 | }) 28 | 29 | t.after(() => target.close()) 30 | 31 | await new Promise(resolve => target.listen({ port: 0 }, resolve)) 32 | 33 | instance.register(From, { 34 | base: `http://localhost:${target.address().port}` 35 | }) 36 | 37 | await new Promise(resolve => instance.listen({ port: 0 }, resolve)) 38 | 39 | const result = await request(`http://localhost:${instance.server.address().port}`) 40 | 41 | t.assert.strictEqual(result.headers['content-type'], 'text/plain') 42 | t.assert.strictEqual(result.headers['x-my-header'], 'hello!') 43 | t.assert.strictEqual(result.statusCode, 205) 44 | t.assert.strictEqual(await result.body.text(), 'hello world') 45 | }) 46 | -------------------------------------------------------------------------------- /test/no-body-opts-with-get.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const t = require('node:test') 4 | const Fastify = require('fastify') 5 | const { request } = require('undici') 6 | const From = require('..') 7 | const http = require('node:http') 8 | 9 | const instance = Fastify() 10 | 11 | t.test('no body opts with get', async (t) => { 12 | t.plan(3) 13 | t.after(() => instance.close()) 14 | 15 | const target = http.createServer((_req, res) => { 16 | t.fail('this should never get called') 17 | res.end('hello world') 18 | }) 19 | 20 | instance.get('/', (_request, reply) => { 21 | try { 22 | reply.from(null, { body: 'this is the new body' }) 23 | } catch (e) { 24 | t.assert.strictEqual(e.message, 'Rewriting the body when doing a GET is not allowed') 25 | reply.send('hello world') 26 | } 27 | }) 28 | 29 | t.after(() => target.close()) 30 | 31 | await new Promise(resolve => target.listen({ port: 0 }, resolve)) 32 | 33 | instance.register(From, { 34 | base: `http://localhost:${target.address().port}` 35 | }) 36 | 37 | await new Promise(resolve => instance.listen({ port: 0 }, resolve)) 38 | 39 | const result = await request(`http://localhost:${instance.server.address().port}`) 40 | 41 | t.assert.strictEqual(result.statusCode, 200) 42 | t.assert.strictEqual(await result.body.text(), 'hello world') 43 | }) 44 | -------------------------------------------------------------------------------- /test/no-body-opts-with-head.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const t = require('node:test') 4 | const Fastify = require('fastify') 5 | const { request } = require('undici') 6 | const From = require('..') 7 | const http = require('node:http') 8 | 9 | const instance = Fastify() 10 | 11 | t.test('no body opts with head', async (t) => { 12 | t.plan(4) 13 | t.after(() => instance.close()) 14 | 15 | const target = http.createServer((_req, res) => { 16 | t.fail('this should never get called') 17 | res.end('hello world') 18 | }) 19 | 20 | instance.head('/', (_request, reply) => { 21 | try { 22 | reply.from(null, { body: 'this is the new body' }) 23 | } catch (e) { 24 | t.assert.strictEqual(e.message, 'Rewriting the body when doing a HEAD is not allowed') 25 | reply.header('x-http-error', '1') 26 | reply.send('hello world') 27 | } 28 | }) 29 | 30 | t.after(() => target.close()) 31 | 32 | await new Promise(resolve => target.listen({ port: 0 }, resolve)) 33 | 34 | instance.register(From, { 35 | base: `http://localhost:${target.address().port}` 36 | }) 37 | 38 | await new Promise(resolve => instance.listen({ port: 0 }, resolve)) 39 | 40 | const result = await request(`http://localhost:${instance.server.address().port}`, { 41 | method: 'HEAD' 42 | }) 43 | 44 | t.assert.strictEqual(result.statusCode, 200) 45 | t.assert.strictEqual(result.headers['x-http-error'], '1') 46 | t.assert.strictEqual(await result.body.text(), '') 47 | }) 48 | -------------------------------------------------------------------------------- /test/no-stream-body-option.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const t = require('node:test') 4 | const Fastify = require('fastify') 5 | const { request } = require('undici') 6 | const From = require('..') 7 | const http = require('node:http') 8 | const Readable = require('node:stream').Readable 9 | 10 | const instance = Fastify() 11 | instance.register(From) 12 | 13 | t.test('no stream body option', async (t) => { 14 | t.plan(2) 15 | t.after(() => instance.close()) 16 | 17 | const target = http.createServer((_req, res) => { 18 | t.fail('the target server should never be called') 19 | res.end() 20 | }) 21 | 22 | instance.post('/', (_request, reply) => { 23 | const body = new Readable({ 24 | read: function () { 25 | t.fail('the read function should never be called') 26 | } 27 | }) 28 | 29 | t.assert.throws(() => { 30 | reply.from(`http://localhost:${target.address().port}`, { 31 | body 32 | }) 33 | }) 34 | 35 | // return a 500 36 | reply.code(500).send({ an: 'error' }) 37 | }) 38 | 39 | t.after(() => target.close()) 40 | 41 | await new Promise(resolve => instance.listen({ port: 0 }, resolve)) 42 | 43 | await new Promise(resolve => target.listen({ port: 0 }, resolve)) 44 | 45 | const result = await request(`http://localhost:${instance.server.address().port}`, { 46 | method: 'POST', 47 | headers: { 48 | 'content-type': 'application/json' 49 | }, 50 | body: JSON.stringify({ 51 | hello: 'world' 52 | }) 53 | }) 54 | 55 | t.assert.strictEqual(result.statusCode, 500) 56 | }) 57 | -------------------------------------------------------------------------------- /test/on-error.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const t = require('node:test') 4 | const Fastify = require('fastify') 5 | const { request, Agent } = require('undici') 6 | const From = require('..') 7 | const FakeTimers = require('@sinonjs/fake-timers') 8 | 9 | const clock = FakeTimers.createClock() 10 | 11 | t.test('on-error', async (t) => { 12 | const target = Fastify() 13 | t.after(() => target.close()) 14 | 15 | target.get('/', (_request, reply) => { 16 | t.assert.ok('request arrives') 17 | 18 | clock.setTimeout(() => { 19 | reply.status(200).send('hello world') 20 | }, 1000) 21 | }) 22 | 23 | await target.listen({ port: 0 }) 24 | 25 | const instance = Fastify() 26 | t.after(() => instance.close()) 27 | 28 | instance.register(From, { http: { requestOptions: { timeout: 100 } } }) 29 | 30 | instance.get('/', (_request, reply) => { 31 | reply.from(`http://localhost:${target.server.address().port}/`, 32 | { 33 | onError: (reply, { error: { stack, ...errorContent } }) => { 34 | t.assert.deepStrictEqual(errorContent, { 35 | statusCode: 504, 36 | name: 'FastifyError', 37 | code: 'FST_REPLY_FROM_GATEWAY_TIMEOUT', 38 | message: 'Gateway Timeout' 39 | }) 40 | reply.code(errorContent.statusCode).send(errorContent) 41 | } 42 | }) 43 | }) 44 | 45 | await instance.listen({ port: 0 }) 46 | 47 | const result = await request(`http://localhost:${instance.server.address().port}/`, { 48 | dispatcher: new Agent({ 49 | pipelining: 0 50 | }) 51 | }) 52 | 53 | t.assert.strictEqual(result.statusCode, 504) 54 | t.assert.match(result.headers['content-type'], /application\/json/) 55 | t.assert.deepStrictEqual(await result.body.json(), { 56 | statusCode: 504, 57 | code: 'FST_REPLY_FROM_GATEWAY_TIMEOUT', 58 | name: 'FastifyError', 59 | message: 'Gateway Timeout' 60 | }) 61 | clock.tick(1000) 62 | }) 63 | -------------------------------------------------------------------------------- /test/on-invalid-upstream-response.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const t = require('node:test') 4 | const Fastify = require('fastify') 5 | const { request } = require('undici') 6 | const From = require('..') 7 | const http = require('node:http') 8 | 9 | const instance = Fastify() 10 | instance.register(From) 11 | 12 | t.test('on-invalid-upstream-response', async (t) => { 13 | t.plan(5) 14 | t.after(() => instance.close()) 15 | 16 | const target = http.createServer((req, res) => { 17 | t.assert.ok('request proxied') 18 | t.assert.strictEqual(req.method, 'GET') 19 | res.statusCode = 888 20 | res.end('non-standard status code') 21 | }) 22 | 23 | instance.get('/', (_, reply) => { 24 | reply.from(`http://localhost:${target.address().port}`, { 25 | onResponse: (_, _reply, res) => { 26 | t.assert.strictEqual(res.statusCode, 888) 27 | } 28 | }) 29 | }) 30 | 31 | t.after(() => target.close()) 32 | 33 | await new Promise(resolve => instance.listen({ port: 0 }, resolve)) 34 | 35 | await new Promise(resolve => target.listen({ port: 0 }, resolve)) 36 | 37 | const result = await request(`http://localhost:${instance.server.address().port}`) 38 | t.assert.strictEqual(result.statusCode, 502) 39 | t.assert.deepStrictEqual(await result.body.json(), { 40 | statusCode: 502, 41 | code: 'FST_REPLY_FROM_BAD_GATEWAY', 42 | error: 'Bad Gateway', 43 | message: 'Bad Gateway' 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /test/onResponse.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const t = require('node:test') 4 | const Fastify = require('fastify') 5 | const { request } = require('undici') 6 | const From = require('..') 7 | const http = require('node:http') 8 | 9 | const instance = Fastify() 10 | instance.register(From) 11 | 12 | t.test('onResponse', async (t) => { 13 | t.plan(6) 14 | t.after(() => instance.close()) 15 | 16 | const target = http.createServer((req, res) => { 17 | t.assert.ok('request proxied') 18 | t.assert.strictEqual(req.method, 'GET') 19 | res.statusCode = 200 20 | res.end('hello world') 21 | }) 22 | 23 | instance.get('/', (request1, reply) => { 24 | reply.from(`http://localhost:${target.address().port}`, { 25 | onResponse: (request2, reply, res) => { 26 | t.assert.strictEqual(res.statusCode, 200) 27 | t.assert.strictEqual(request1.raw, request2.raw) 28 | reply.send(res.stream) 29 | } 30 | }) 31 | }) 32 | 33 | t.after(() => target.close()) 34 | 35 | await new Promise(resolve => instance.listen({ port: 0 }, resolve)) 36 | 37 | await new Promise(resolve => target.listen({ port: 0 }, resolve)) 38 | 39 | const result = await request(`http://localhost:${instance.server.address().port}`) 40 | t.assert.strictEqual(result.statusCode, 200) 41 | t.assert.strictEqual(await result.body.text(), 'hello world') 42 | }) 43 | -------------------------------------------------------------------------------- /test/padded-body.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const t = require('node:test') 4 | const Fastify = require('fastify') 5 | const { request } = require('undici') 6 | const From = require('..') 7 | const http = require('node:http') 8 | 9 | const instance = Fastify() 10 | instance.register(From) 11 | 12 | t.test('padded body', async (t) => { 13 | t.plan(6) 14 | t.after(() => instance.close()) 15 | 16 | const bodyString = `{ 17 | "hello": "world" 18 | }` 19 | 20 | const parsedLength = Buffer.byteLength(JSON.stringify(JSON.parse(bodyString))) 21 | 22 | const target = http.createServer((req, res) => { 23 | t.assert.ok('request proxied') 24 | t.assert.strictEqual(req.method, 'POST') 25 | t.assert.strictEqual(req.headers['content-type'], 'application/json') 26 | t.assert.deepStrictEqual(req.headers['content-length'], `${parsedLength}`) 27 | let data = '' 28 | req.setEncoding('utf8') 29 | req.on('data', (d) => { 30 | data += d 31 | }) 32 | req.on('end', () => { 33 | t.assert.deepStrictEqual(JSON.parse(data), { hello: 'world' }) 34 | res.statusCode = 200 35 | res.setHeader('content-type', 'application/json') 36 | res.end(JSON.stringify({ something: 'else' })) 37 | }) 38 | }) 39 | 40 | instance.post('/', (_request, reply) => { 41 | reply.from(`http://localhost:${target.address().port}`) 42 | }) 43 | 44 | t.after(() => target.close()) 45 | 46 | await new Promise(resolve => instance.listen({ port: 0 }, resolve)) 47 | 48 | await new Promise(resolve => target.listen({ port: 0 }, resolve)) 49 | 50 | const result = await request(`http://localhost:${instance.server.address().port}`, { 51 | method: 'POST', 52 | headers: { 53 | 'content-type': 'application/json' 54 | }, 55 | body: bodyString 56 | }) 57 | 58 | t.assert.deepStrictEqual(await result.body.json(), { something: 'else' }) 59 | }) 60 | -------------------------------------------------------------------------------- /test/post-formbody.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const t = require('node:test') 4 | const Fastify = require('fastify') 5 | const { request } = require('undici') 6 | const From = require('..') 7 | const http = require('node:http') 8 | 9 | const instance = Fastify() 10 | instance.register(From, { 11 | contentTypesToEncode: ['application/x-www-form-urlencoded'] 12 | }) 13 | instance.register(require('@fastify/formbody')) 14 | 15 | t.test('post-formbody', async (t) => { 16 | t.plan(6) 17 | t.after(() => instance.close()) 18 | 19 | const target = http.createServer((req, res) => { 20 | t.assert.ok('request proxied') 21 | t.assert.strictEqual(req.method, 'POST') 22 | t.assert.strictEqual(req.headers['content-type'], 'application/x-www-form-urlencoded') 23 | let data = '' 24 | req.setEncoding('utf8') 25 | req.on('data', (d) => { 26 | data += d 27 | }) 28 | req.on('end', () => { 29 | const str = data.toString() 30 | t.assert.deepStrictEqual(JSON.parse(data), { some: 'info', another: 'detail' }) 31 | res.statusCode = 200 32 | res.setHeader('content-type', 'application/x-www-form-urlencoded') 33 | res.end(str) 34 | }) 35 | }) 36 | 37 | instance.post('/', (_request, reply) => { 38 | reply.from(`http://localhost:${target.address().port}`) 39 | }) 40 | 41 | t.after(() => target.close()) 42 | 43 | await new Promise(resolve => instance.listen({ port: 0 }, resolve)) 44 | await new Promise(resolve => target.listen({ port: 0 }, resolve)) 45 | 46 | const result = await request(`http://localhost:${instance.server.address().port}`, { 47 | method: 'POST', 48 | headers: { 'content-type': 'application/x-www-form-urlencoded' }, 49 | body: 'some=info&another=detail' 50 | }) 51 | 52 | t.assert.strictEqual(result.headers['content-type'], 'application/x-www-form-urlencoded') 53 | t.assert.deepStrictEqual(await result.body.json(), { some: 'info', another: 'detail' }) 54 | }) 55 | -------------------------------------------------------------------------------- /test/post-plain-text.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const t = require('node:test') 4 | const Fastify = require('fastify') 5 | const { request } = require('undici') 6 | const From = require('..') 7 | const http = require('node:http') 8 | 9 | const instance = Fastify() 10 | instance.register(From) 11 | 12 | t.test('post plain text', async (t) => { 13 | t.plan(6) 14 | t.after(() => instance.close()) 15 | 16 | const target = http.createServer((req, res) => { 17 | t.assert.ok('request proxied') 18 | t.assert.strictEqual(req.method, 'POST') 19 | t.assert.strictEqual(req.headers['content-type'], 'text/plain') 20 | let data = '' 21 | req.setEncoding('utf8') 22 | req.on('data', (d) => { 23 | data += d 24 | }) 25 | req.on('end', () => { 26 | const str = data.toString() 27 | t.assert.deepStrictEqual(str, 'this is plain text') 28 | res.statusCode = 200 29 | res.setHeader('content-type', 'text/plain') 30 | res.end(str) 31 | }) 32 | }) 33 | 34 | instance.post('/', (_request, reply) => { 35 | reply.from(`http://localhost:${target.address().port}`) 36 | }) 37 | 38 | t.after(() => target.close()) 39 | 40 | await new Promise(resolve => instance.listen({ port: 0 }, resolve)) 41 | 42 | await new Promise(resolve => target.listen({ port: 0 }, resolve)) 43 | 44 | const result = await request(`http://localhost:${instance.server.address().port}`, { 45 | method: 'POST', 46 | headers: { 'content-type': 'text/plain' }, 47 | body: 'this is plain text' 48 | }) 49 | 50 | t.assert.strictEqual(result.headers['content-type'], 'text/plain') 51 | t.assert.deepStrictEqual(await result.body.text(), 'this is plain text') 52 | }) 53 | -------------------------------------------------------------------------------- /test/post-with-custom-encoded-contenttype.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const t = require('node:test') 4 | const Fastify = require('fastify') 5 | const { request } = require('undici') 6 | const From = require('..') 7 | const http = require('node:http') 8 | const { parse } = require('node:querystring') 9 | 10 | const instance = Fastify() 11 | instance.register(From, { 12 | contentTypesToEncode: ['application/x-www-form-urlencoded'] 13 | }) 14 | 15 | instance.addContentTypeParser( 16 | 'application/x-www-form-urlencoded', 17 | { parseAs: 'buffer', bodyLimit: 1000 }, 18 | (_req, body, done) => done(null, parse(body.toString())) 19 | ) 20 | 21 | t.test('post with custom encoded content-type', async (t) => { 22 | t.plan(6) 23 | t.after(() => instance.close()) 24 | 25 | const target = http.createServer((req, res) => { 26 | t.assert.ok('request proxied') 27 | t.assert.strictEqual(req.method, 'POST') 28 | t.assert.strictEqual(req.headers['content-type'], 'application/x-www-form-urlencoded') 29 | let data = '' 30 | req.setEncoding('utf8') 31 | req.on('data', (d) => { 32 | data += d 33 | }) 34 | req.on('end', () => { 35 | const str = data.toString() 36 | t.assert.deepStrictEqual(JSON.parse(data), { some: 'info', another: 'detail' }) 37 | res.statusCode = 200 38 | res.setHeader('content-type', 'application/x-www-form-urlencoded') 39 | res.end(str) 40 | }) 41 | }) 42 | 43 | instance.post('/', (_request, reply) => { 44 | reply.from(`http://localhost:${target.address().port}`) 45 | }) 46 | 47 | t.after(() => target.close()) 48 | 49 | await new Promise(resolve => instance.listen({ port: 0 }, resolve)) 50 | 51 | await new Promise(resolve => target.listen({ port: 0 }, resolve)) 52 | 53 | const result = await request(`http://localhost:${instance.server.address().port}`, { 54 | method: 'POST', 55 | headers: { 56 | 'content-type': 'application/x-www-form-urlencoded' 57 | }, 58 | body: 'some=info&another=detail' 59 | }) 60 | 61 | t.assert.strictEqual(result.headers['content-type'], 'application/x-www-form-urlencoded') 62 | t.assert.deepStrictEqual(await result.body.json(), { some: 'info', another: 'detail' }) 63 | }) 64 | -------------------------------------------------------------------------------- /test/post-with-octet-stream.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const Fastify = require('fastify') 5 | const { request } = require('undici') 6 | const From = require('../index') 7 | const http = require('node:http') 8 | const { parse } = require('node:querystring') 9 | 10 | test('with explicitly set content-type application/octet-stream', async t => { 11 | const instance = Fastify() 12 | instance.register(From, { 13 | contentTypesToEncode: ['application/octet-stream'] 14 | }) 15 | 16 | instance.addContentTypeParser( 17 | 'application/octet-stream', 18 | { parseAs: 'buffer', bodyLimit: 1000 }, 19 | (_req, body, done) => done(null, parse(body.toString())) 20 | ) 21 | 22 | t.plan(6) 23 | t.after(() => instance.close()) 24 | 25 | const target = http.createServer((req, res) => { 26 | t.assert.ok('request proxied') 27 | t.assert.strictEqual(req.method, 'POST') 28 | t.assert.strictEqual(req.headers['content-type'], 'application/octet-stream') 29 | let data = '' 30 | req.setEncoding('utf8') 31 | req.on('data', (d) => { 32 | data += d 33 | }) 34 | req.on('end', () => { 35 | const str = data.toString() 36 | t.assert.deepStrictEqual(JSON.parse(data), { some: 'info', another: 'detail' }) 37 | res.statusCode = 200 38 | res.setHeader('content-type', 'application/octet-stream') 39 | res.end(str) 40 | }) 41 | }) 42 | 43 | instance.post('/', (_request, reply) => { 44 | reply.from(`http://localhost:${target.address().port}`) 45 | }) 46 | 47 | t.after(() => target.close()) 48 | 49 | await new Promise(resolve => instance.listen({ port: 0 }, resolve)) 50 | 51 | await new Promise(resolve => target.listen({ port: 0 }, resolve)) 52 | 53 | const result = await request(`http://localhost:${instance.server.address().port}`, { 54 | method: 'POST', 55 | headers: { 'content-type': 'application/octet-stream' }, 56 | body: 'some=info&another=detail' 57 | }) 58 | 59 | t.assert.strictEqual(result.headers['content-type'], 'application/octet-stream') 60 | t.assert.deepStrictEqual(await result.body.json(), { some: 'info', another: 'detail' }) 61 | }) 62 | -------------------------------------------------------------------------------- /test/retry-on-503.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const Fastify = require('fastify') 5 | const { request } = require('undici') 6 | const From = require('..') 7 | const http = require('node:http') 8 | 9 | function createTargetServer (withRetryAfterHeader, stopAfter = 1) { 10 | let requestCount = 0 11 | return http.createServer((_req, res) => { 12 | if (requestCount++ < stopAfter) { 13 | res.statusCode = 503 14 | res.setHeader('Content-Type', 'text/plain') 15 | if (withRetryAfterHeader) { 16 | res.setHeader('Retry-After', 100) 17 | } 18 | return res.end('This Service is Unavailable') 19 | } 20 | res.statusCode = 205 21 | res.setHeader('Content-Type', 'text/plain') 22 | return res.end(`Hello World ${requestCount}!`) 23 | }) 24 | } 25 | 26 | test('Should retry on 503 HTTP error', async function (t) { 27 | t.plan(3) 28 | const target = createTargetServer() 29 | await new Promise(resolve => target.listen({ port: 0 }, resolve)) 30 | t.after(() => target.close()) 31 | 32 | const instance = Fastify() 33 | 34 | instance.register(From, { 35 | base: `http://localhost:${target.address().port}` 36 | }) 37 | 38 | instance.get('/', (_request, reply) => { 39 | reply.from() 40 | }) 41 | 42 | t.after(() => instance.close()) 43 | await instance.listen({ port: 0 }) 44 | 45 | const res = await request(`http://localhost:${instance.server.address().port}`) 46 | t.assert.strictEqual(res.headers['content-type'], 'text/plain') 47 | t.assert.strictEqual(res.statusCode, 205) 48 | t.assert.strictEqual(await res.body.text(), 'Hello World 2!') 49 | }) 50 | 51 | test('Should retry on 503 HTTP error with Retry-After response header', async function (t) { 52 | t.plan(3) 53 | const target = createTargetServer(true) 54 | await new Promise(resolve => target.listen({ port: 0 }, resolve)) 55 | t.after(() => target.close()) 56 | 57 | const instance = Fastify() 58 | 59 | instance.register(From, { 60 | base: `http://localhost:${target.address().port}` 61 | }) 62 | 63 | instance.get('/', (_request, reply) => { 64 | reply.from() 65 | }) 66 | 67 | t.after(() => instance.close()) 68 | await instance.listen({ port: 0 }) 69 | 70 | const res = await request(`http://localhost:${instance.server.address().port}`) 71 | t.assert.strictEqual(res.headers['content-type'], 'text/plain') 72 | t.assert.strictEqual(res.statusCode, 205) 73 | t.assert.strictEqual(await res.body.text(), 'Hello World 2!') 74 | }) 75 | 76 | test('Should abort if server is always returning 503', async function (t) { 77 | t.plan(2) 78 | const target = createTargetServer(true, Number.MAX_SAFE_INTEGER) 79 | await new Promise(resolve => target.listen({ port: 0 }, resolve)) 80 | t.after(() => target.close()) 81 | 82 | const instance = Fastify() 83 | 84 | instance.register(From, { 85 | base: `http://localhost:${target.address().port}` 86 | }) 87 | 88 | instance.get('/', (_request, reply) => { 89 | reply.from() 90 | }) 91 | 92 | t.after(() => instance.close()) 93 | await instance.listen({ port: 0 }) 94 | 95 | await request(`http://localhost:${instance.server.address().port}`) 96 | await request(`http://localhost:${instance.server.address().port}`) 97 | await request(`http://localhost:${instance.server.address().port}`) 98 | await request(`http://localhost:${instance.server.address().port}`) 99 | const result = await request(`http://localhost:${instance.server.address().port}`) 100 | t.assert.strictEqual(result.statusCode, 503) 101 | t.assert.strictEqual(await result.body.text(), 'This Service is Unavailable') 102 | }) 103 | -------------------------------------------------------------------------------- /test/retry-with-a-custom-handler.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const Fastify = require('fastify') 5 | const { request, Agent } = require('undici') 6 | const From = require('..') 7 | const http = require('node:http') 8 | 9 | function serverWithCustomError (stopAfter, statusCodeToFailOn, closeSocket) { 10 | let requestCount = 0 11 | return http.createServer((req, res) => { 12 | if (requestCount++ < stopAfter) { 13 | if (closeSocket) req.socket.end() 14 | res.statusCode = statusCodeToFailOn 15 | res.setHeader('Content-Type', 'text/plain') 16 | return res.end('This Service is Unavailable') 17 | } else { 18 | res.statusCode = 205 19 | res.setHeader('Content-Type', 'text/plain') 20 | return res.end(`Hello World ${requestCount}!`) 21 | } 22 | }) 23 | } 24 | 25 | async function setupServer (t, fromOptions = {}, statusCodeToFailOn = 500, stopAfter = 4, closeSocket = false) { 26 | const target = serverWithCustomError(stopAfter, statusCodeToFailOn, closeSocket) 27 | 28 | await target.listen({ port: 0 }) 29 | t.after(() => target.close()) 30 | 31 | const instance = Fastify() 32 | instance.register(From, { 33 | base: `http://localhost:${target.address().port}` 34 | }) 35 | 36 | instance.get('/', (_request, reply) => { 37 | reply.from(`http://localhost:${target.address().port}`, fromOptions) 38 | }) 39 | 40 | t.after(() => instance.close()) 41 | await instance.listen({ port: 0 }) 42 | 43 | return { 44 | instance 45 | } 46 | } 47 | 48 | test('a 500 status code with no custom handler should fail', async (t) => { 49 | const { instance } = await setupServer(t) 50 | 51 | const result = await request(`http://localhost:${instance.server.address().port}`, { dispatcher: new Agent({ pipelining: 3 }) }) 52 | 53 | t.assert.strictEqual(result.statusCode, 500) 54 | t.assert.strictEqual(await result.body.text(), 'This Service is Unavailable') 55 | }) 56 | 57 | test("a server 500's with a custom handler and should revive", async (t) => { 58 | const customRetryLogic = ({ req, res, getDefaultDelay }) => { 59 | const defaultDelay = getDefaultDelay() 60 | if (defaultDelay) return defaultDelay 61 | 62 | if (res && res.statusCode === 500 && req.method === 'GET') { 63 | return 0.1 64 | } 65 | return null 66 | } 67 | 68 | const { instance } = await setupServer(t, { retryDelay: customRetryLogic }) 69 | 70 | const res = await request(`http://localhost:${instance.server.address().port}`) 71 | 72 | t.assert.strictEqual(res.headers['content-type'], 'text/plain') 73 | t.assert.strictEqual(res.statusCode, 205) 74 | t.assert.strictEqual(await res.body.text(), 'Hello World 5!') 75 | }) 76 | 77 | test('custom retry does not invoke the default delay causing a 501', async (t) => { 78 | // the key here is our retryDelay doesn't register the deefault handler and as a result it doesn't work 79 | const customRetryLogic = ({ req, res }) => { 80 | if (res && res.statusCode === 500 && req.method === 'GET') { 81 | return 0 82 | } 83 | return null 84 | } 85 | 86 | const { instance } = await setupServer(t, { retryDelay: customRetryLogic }, 501) 87 | 88 | const res = await request(`http://localhost:${instance.server.address().port}`) 89 | 90 | t.assert.strictEqual(res.statusCode, 501) 91 | t.assert.strictEqual(await res.body.text(), 'This Service is Unavailable') 92 | }) 93 | 94 | test('custom retry delay functions can invoke the default delay', async (t) => { 95 | const customRetryLogic = ({ req, res, getDefaultDelay }) => { 96 | // registering the default retry logic for non 500 errors if it occurs 97 | const defaultDelay = getDefaultDelay() 98 | if (defaultDelay) return defaultDelay 99 | 100 | if (res && res.statusCode === 500 && req.method === 'GET') { 101 | return 0.1 102 | } 103 | 104 | return null 105 | } 106 | 107 | const { instance } = await setupServer(t, { retryDelay: customRetryLogic }, 500) 108 | 109 | const res = await request(`http://localhost:${instance.server.address().port}`) 110 | 111 | t.assert.strictEqual(res.headers['content-type'], 'text/plain') 112 | t.assert.strictEqual(res.statusCode, 205) 113 | t.assert.strictEqual(await res.body.text(), 'Hello World 5!') 114 | }) 115 | 116 | test('custom retry delay function inspects the err paramater', async (t) => { 117 | const customRetryLogic = ({ err }) => { 118 | if (err && (err.code === 'UND_ERR_SOCKET' || err.code === 'ECONNRESET')) { 119 | return 0.1 120 | } 121 | return null 122 | } 123 | 124 | const { instance } = await setupServer(t, { retryDelay: customRetryLogic }, 500, 4, true) 125 | 126 | const res = await request(`http://localhost:${instance.server.address().port}`) 127 | 128 | t.assert.strictEqual(res.headers['content-type'], 'text/plain') 129 | t.assert.strictEqual(res.statusCode, 205) 130 | t.assert.strictEqual(await res.body.text(), 'Hello World 5!') 131 | }) 132 | 133 | test('we can exceed our retryCount and introspect attempts independently', async (t) => { 134 | const attemptCounter = [] 135 | 136 | const customRetryLogic = ({ err, attempt }) => { 137 | attemptCounter.push(attempt) 138 | 139 | if (err && (err.code === 'UND_ERR_SOCKET' || err.code === 'ECONNRESET')) { 140 | return 0.1 141 | } 142 | 143 | return null 144 | } 145 | 146 | const { instance } = await setupServer(t, { retryDelay: customRetryLogic }, 500, 4, true) 147 | 148 | const res = await request(`http://localhost:${instance.server.address().port}`) 149 | 150 | t.assert.deepStrictEqual(attemptCounter, [0, 1, 2, 3, 4]) 151 | t.assert.strictEqual(res.headers['content-type'], 'text/plain') 152 | t.assert.strictEqual(res.statusCode, 205) 153 | t.assert.strictEqual(await res.body.text(), 'Hello World 5!') 154 | }) 155 | 156 | test('we handle our retries based on the retryCount', async (t) => { 157 | const attemptCounter = [] 158 | const customRetryLogic = ({ req, res, attempt, retriesCount }) => { 159 | if (retriesCount < attempt) { 160 | return null 161 | } 162 | 163 | if (res && res.statusCode === 500 && req.method === 'GET') { 164 | attemptCounter.push(attempt) 165 | return 0.1 166 | } 167 | return null 168 | } 169 | 170 | const { instance } = await setupServer(t, { retryDelay: customRetryLogic, retriesCount: 2 }, 500) 171 | 172 | await request(`http://localhost:${instance.server.address().port}`) 173 | const res = await request(`http://localhost:${instance.server.address().port}`) 174 | 175 | t.assert.deepStrictEqual(attemptCounter.slice(0, 2), [0, 1]) 176 | t.assert.strictEqual(res.headers['content-type'], 'text/plain') 177 | t.assert.strictEqual(res.statusCode, 205) 178 | t.assert.strictEqual(await res.body.text(), 'Hello World 5!') 179 | }) 180 | -------------------------------------------------------------------------------- /test/rewrite-headers-type.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const t = require('node:test') 4 | const Fastify = require('fastify') 5 | const { request } = require('undici') 6 | const From = require('..') 7 | const http = require('node:http') 8 | 9 | const instance = Fastify() 10 | instance.register(From) 11 | 12 | t.test('rewriteHeaders type', async (t) => { 13 | t.plan(5) 14 | t.after(() => instance.close()) 15 | 16 | const target = http.createServer((req, res) => { 17 | t.assert.ok('request proxied') 18 | t.assert.strictEqual(req.method, 'GET') 19 | res.statusCode = 205 20 | res.end('hello world') 21 | }) 22 | 23 | instance.get('/', (request, reply) => { 24 | reply.from(`http://localhost:${target.address().port}`, { 25 | rewriteHeaders: (_headers, req) => { 26 | t.assert.ok('rewriteHeaders called with correct request parameter') 27 | t.assert.strictEqual(req, request) 28 | return {} 29 | } 30 | }) 31 | }) 32 | 33 | t.after(() => target.close()) 34 | 35 | await new Promise(resolve => instance.listen({ port: 0 }, resolve)) 36 | await new Promise(resolve => target.listen({ port: 0 }, resolve)) 37 | 38 | const result = await request(`http://localhost:${instance.server.address().port}`) 39 | t.assert.strictEqual(result.statusCode, 205) 40 | }) 41 | -------------------------------------------------------------------------------- /test/rewrite-headers.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const t = require('node:test') 4 | const Fastify = require('fastify') 5 | const { request } = require('undici') 6 | const From = require('..') 7 | const http = require('node:http') 8 | 9 | const instance = Fastify() 10 | instance.register(From) 11 | 12 | t.test('rewriteHeaders', async (t) => { 13 | t.plan(7) 14 | t.after(() => instance.close()) 15 | 16 | const target = http.createServer((req, res) => { 17 | t.assert.ok('request proxied') 18 | t.assert.strictEqual(req.method, 'GET') 19 | res.statusCode = 205 20 | res.setHeader('Content-Type', 'text/plain') 21 | res.setHeader('x-my-header', 'hello!') 22 | res.end('hello world') 23 | }) 24 | 25 | instance.get('/', (_request, reply) => { 26 | reply.from(`http://localhost:${target.address().port}`, { 27 | rewriteHeaders: (headers) => { 28 | t.assert.ok('rewriteHeaders called') 29 | return { 30 | 'content-type': headers['content-type'], 31 | 'x-another-header': 'so headers!' 32 | } 33 | } 34 | }) 35 | }) 36 | 37 | t.after(() => target.close()) 38 | 39 | await new Promise(resolve => instance.listen({ port: 0 }, resolve)) 40 | await new Promise(resolve => target.listen({ port: 0 }, resolve)) 41 | 42 | const result = await request(`http://localhost:${instance.server.address().port}`) 43 | t.assert.strictEqual(result.headers['content-type'], 'text/plain') 44 | t.assert.strictEqual(result.headers['x-another-header'], 'so headers!') 45 | t.assert.ok(!result.headers['x-my-header']) 46 | t.assert.strictEqual(result.statusCode, 205) 47 | }) 48 | -------------------------------------------------------------------------------- /test/rewrite-request-headers-type.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const t = require('node:test') 4 | const Fastify = require('fastify') 5 | const { request } = require('undici') 6 | const From = require('..') 7 | const http = require('node:http') 8 | 9 | const instance = Fastify() 10 | instance.register(From) 11 | 12 | t.test('rewriteRequestHeaders type', async (t) => { 13 | t.plan(5) 14 | t.after(() => instance.close()) 15 | 16 | const target = http.createServer((req, res) => { 17 | t.assert.ok('request proxied') 18 | t.assert.strictEqual(req.method, 'GET') 19 | res.statusCode = 205 20 | res.end(req.headers.host) 21 | }) 22 | 23 | instance.get('/', (request, reply) => { 24 | reply.from(`http://localhost:${target.address().port}`, { 25 | rewriteRequestHeaders: (originalReq) => { 26 | t.assert.ok('rewriteRequestHeaders called with correct request parameter') 27 | t.assert.strictEqual(originalReq, request) 28 | return {} 29 | } 30 | }) 31 | }) 32 | 33 | t.after(() => target.close()) 34 | 35 | await new Promise((resolve) => instance.listen({ port: 0 }, resolve)) 36 | 37 | await new Promise((resolve) => target.listen({ port: 0 }, resolve)) 38 | 39 | const result = await request(`http://localhost:${instance.server.address().port}`) 40 | 41 | t.assert.strictEqual(result.statusCode, 205) 42 | }) 43 | -------------------------------------------------------------------------------- /test/rewrite-request-headers.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const t = require('node:test') 4 | const Fastify = require('fastify') 5 | const { request } = require('undici') 6 | const From = require('..') 7 | const http = require('node:http') 8 | 9 | const instance = Fastify() 10 | instance.register(From) 11 | 12 | t.test('rewriteRequestHeaders', async (t) => { 13 | t.plan(6) 14 | t.after(() => instance.close()) 15 | 16 | const target = http.createServer((req, res) => { 17 | t.assert.ok('request proxied') 18 | t.assert.strictEqual(req.method, 'GET') 19 | res.statusCode = 205 20 | res.setHeader('Content-Type', 'text/plain') 21 | res.end(req.headers.host) 22 | }) 23 | 24 | instance.get('/', (_request, reply) => { 25 | reply.from(`http://localhost:${target.address().port}`, { 26 | rewriteRequestHeaders: (_originalReq, headers) => { 27 | t.assert.ok('rewriteRequestHeaders called') 28 | return Object.assign(headers, { host: 'host-override' }) 29 | } 30 | }) 31 | }) 32 | 33 | t.after(() => target.close()) 34 | 35 | await new Promise((resolve) => instance.listen({ port: 0 }, resolve)) 36 | await new Promise((resolve) => target.listen({ port: 0 }, resolve)) 37 | 38 | const result = await request(`http://localhost:${instance.server.address().port}`) 39 | 40 | t.assert.strictEqual(result.headers['content-type'], 'text/plain') 41 | t.assert.strictEqual(result.statusCode, 205) 42 | t.assert.strictEqual(await result.body.text(), 'host-override') 43 | }) 44 | -------------------------------------------------------------------------------- /test/transform-body.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const t = require('node:test') 4 | const Fastify = require('fastify') 5 | const { request } = require('undici') 6 | const From = require('..') 7 | const http = require('node:http') 8 | const Transform = require('node:stream').Transform 9 | 10 | const instance = Fastify() 11 | instance.register(From) 12 | 13 | t.test('transform body', async (t) => { 14 | t.plan(6) 15 | t.after(() => instance.close()) 16 | 17 | const target = http.createServer((req, res) => { 18 | t.assert.ok('request proxied') 19 | t.assert.strictEqual(req.method, 'GET') 20 | res.statusCode = 205 21 | res.setHeader('Content-Type', 'text/plain') 22 | res.setHeader('x-my-header', 'hello!') 23 | res.end('hello world') 24 | }) 25 | 26 | instance.get('/', (_request, reply) => { 27 | reply.from(`http://localhost:${target.address().port}`, { 28 | onResponse: (_request, reply, res) => { 29 | reply.send( 30 | res.stream.pipe( 31 | new Transform({ 32 | transform: function (chunk, _enc, cb) { 33 | this.push(chunk.toString().toUpperCase()) 34 | cb() 35 | } 36 | }) 37 | ) 38 | ) 39 | } 40 | }) 41 | }) 42 | 43 | t.after(() => target.close()) 44 | 45 | await new Promise((resolve) => instance.listen({ port: 0 }, resolve)) 46 | 47 | await new Promise((resolve) => target.listen({ port: 0 }, resolve)) 48 | 49 | const result = await request(`http://localhost:${instance.server.address().port}`) 50 | 51 | t.assert.strictEqual(result.headers['content-type'], 'text/plain') 52 | t.assert.strictEqual(result.headers['x-my-header'], 'hello!') 53 | t.assert.strictEqual(result.statusCode, 205) 54 | t.assert.strictEqual(await result.body.text(), 'HELLO WORLD') 55 | }) 56 | -------------------------------------------------------------------------------- /test/undici-agent.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const t = require('node:test') 4 | const Fastify = require('fastify') 5 | const undici = require('undici') 6 | const proxyquire = require('proxyquire') 7 | const http = require('node:http') 8 | const { getUndiciOptions } = require('../lib/request') 9 | 10 | t.test('undici agent', async (t) => { 11 | t.plan(6) 12 | 13 | const instance = Fastify() 14 | t.after(() => instance.close()) 15 | 16 | const target = http.createServer((_req, res) => { 17 | res.statusCode = 200 18 | res.end('hello world') 19 | }) 20 | 21 | instance.get('/', (_request, reply) => { 22 | reply.from() 23 | }) 24 | 25 | t.after(() => target.close()) 26 | 27 | await new Promise(resolve => target.listen({ port: 0 }, resolve)) 28 | 29 | let poolCreation = 0 30 | 31 | const From = proxyquire('..', { 32 | './lib/request.js': proxyquire('../lib/request.js', { 33 | undici: proxyquire('undici', { 34 | './lib/dispatcher/agent': proxyquire('undici/lib/dispatcher/agent.js', { 35 | './pool': class Pool extends undici.Pool { 36 | constructor (url, options) { 37 | super(url, options) 38 | poolCreation++ 39 | } 40 | } 41 | }) 42 | }) 43 | }) 44 | }) 45 | 46 | instance.register(From, { 47 | base: `http://localhost:${target.address().port}`, 48 | undici: buildUndiciOptions() 49 | }) 50 | 51 | await new Promise(resolve => instance.listen({ port: 0 }, resolve)) 52 | 53 | const result = await undici.request(`http://localhost:${instance.server.address().port}`) 54 | 55 | t.assert.strictEqual(result.statusCode, 200) 56 | t.assert.strictEqual(await result.body.text(), 'hello world') 57 | t.assert.strictEqual(poolCreation, 1) 58 | 59 | const result2 = await undici.request(`http://localhost:${instance.server.address().port}`) 60 | 61 | t.assert.strictEqual(result2.statusCode, 200) 62 | t.assert.strictEqual(await result2.body.text(), 'hello world') 63 | t.assert.strictEqual(poolCreation, 1) 64 | }) 65 | 66 | function buildUndiciOptions () { 67 | return getUndiciOptions({ 68 | connections: 42, 69 | pipelining: 24, 70 | keepAliveTimeout: 4242, 71 | strictContentLength: false 72 | }) 73 | } 74 | -------------------------------------------------------------------------------- /test/undici-body.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const t = require('node:test') 4 | const Fastify = require('fastify') 5 | const { request } = require('undici') 6 | const From = require('..') 7 | const http = require('node:http') 8 | 9 | const instance = Fastify() 10 | 11 | t.test('undici body', async (t) => { 12 | t.plan(6) 13 | t.after(() => instance.close()) 14 | 15 | const bodyString = JSON.stringify({ hello: 'world' }) 16 | 17 | const parsedLength = Buffer.byteLength(bodyString) 18 | 19 | const target = http.createServer((req, res) => { 20 | t.assert.ok('request proxied') 21 | t.assert.strictEqual(req.method, 'POST') 22 | t.assert.strictEqual(req.headers['content-type'], 'application/json') 23 | t.assert.deepStrictEqual(req.headers['content-length'], `${parsedLength}`) 24 | let data = '' 25 | req.setEncoding('utf8') 26 | req.on('data', (d) => { 27 | data += d 28 | }) 29 | req.on('end', () => { 30 | t.assert.deepStrictEqual(JSON.parse(data), { hello: 'world' }) 31 | res.statusCode = 200 32 | res.setHeader('content-type', 'application/json') 33 | res.end(JSON.stringify({ something: 'else' })) 34 | }) 35 | }) 36 | 37 | instance.post('/', (_request, reply) => { 38 | reply.from(`http://localhost:${target.address().port}`) 39 | }) 40 | 41 | t.after(() => target.close()) 42 | 43 | await new Promise((resolve) => target.listen({ port: 0 }, resolve)) 44 | 45 | instance.addContentTypeParser('application/json', function (_req, payload, done) { 46 | done(null, payload) 47 | }) 48 | 49 | instance.register(From, { 50 | base: `http://localhost:${target.address().port}`, 51 | undici: true 52 | }) 53 | 54 | await new Promise((resolve) => instance.listen({ port: 0 }, resolve)) 55 | 56 | const result = await request(`http://localhost:${instance.server.address().port}`, { 57 | method: 'POST', 58 | headers: { 59 | 'content-type': 'application/json' 60 | }, 61 | body: bodyString 62 | }) 63 | 64 | t.assert.deepStrictEqual(await result.body.json(), { something: 'else' }) 65 | }) 66 | -------------------------------------------------------------------------------- /test/undici-chaining.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const t = require('node:test') 4 | const Fastify = require('fastify') 5 | const { request } = require('undici') 6 | const From = require('..') 7 | 8 | const header = 'attachment; filename="år.pdf"' 9 | 10 | t.test('undici chaining', async (t) => { 11 | t.plan(2) 12 | 13 | const instance = Fastify() 14 | t.after(() => instance.close()) 15 | const proxy1 = Fastify() 16 | t.after(() => proxy1.close()) 17 | const proxy2 = Fastify() 18 | t.after(() => proxy2.close()) 19 | 20 | instance.get('/', (_request, reply) => { 21 | reply.header('content-disposition', header).send('OK') 22 | }) 23 | 24 | proxy1.register(From, { 25 | undici: { 26 | keepAliveMaxTimeout: 10 27 | } 28 | }) 29 | proxy1.get('/', (_request, reply) => { 30 | return reply.from(`http://localhost:${instance.server.address().port}`) 31 | }) 32 | 33 | proxy2.register(From, { 34 | undici: { 35 | keepAliveMaxTimeout: 10 36 | } 37 | }) 38 | proxy2.get('/', (_request, reply) => { 39 | return reply.from(`http://localhost:${proxy1.server.address().port}`) 40 | }) 41 | 42 | await new Promise((resolve) => instance.listen({ port: 0 }, resolve)) 43 | await new Promise((resolve) => proxy1.listen({ port: 0 }, resolve)) 44 | await new Promise((resolve) => proxy2.listen({ port: 0 }, resolve)) 45 | 46 | const result = await request(`http://localhost:${proxy2.server.address().port}`) 47 | 48 | t.assert.strictEqual(result.statusCode, 200) 49 | t.assert.strictEqual(await result.body.text(), 'OK') 50 | }) 51 | -------------------------------------------------------------------------------- /test/undici-connect-timeout.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const t = require('node:test') 4 | const http = require('node:http') 5 | const net = require('node:net') 6 | const Fastify = require('fastify') 7 | const From = require('..') 8 | const { request, Agent } = require('undici') 9 | 10 | t.test('undici connect timeout', async (t) => { 11 | // never connect 12 | net.connect = function (options) { 13 | return new net.Socket(options) 14 | } 15 | 16 | const target = http.createServer(() => { 17 | t.fail('target never called') 18 | }) 19 | 20 | t.plan(2) 21 | await new Promise(resolve => target.listen({ port: 0 }, resolve)) 22 | 23 | const instance = Fastify() 24 | t.after(() => instance.close()) 25 | t.after(() => target.close()) 26 | 27 | instance.register(From, { 28 | base: `http://localhost:${target.address().port}`, 29 | undici: { 30 | connectTimeout: 50 31 | } 32 | }) 33 | 34 | instance.get('/', (_request, reply) => { 35 | reply.from() 36 | }) 37 | 38 | await instance.listen({ port: 0 }) 39 | 40 | try { 41 | await request(`http://localhost:${instance.server.address().port}/`, { 42 | dispatcher: new Agent({ 43 | pipelining: 0, 44 | connectTimeout: 10 45 | }) 46 | }) 47 | } catch (err) { 48 | t.assert.strictEqual(err.code, 'UND_ERR_CONNECT_TIMEOUT') 49 | t.assert.strictEqual(err.name, 'ConnectTimeoutError') 50 | return 51 | } 52 | 53 | t.fail() 54 | }) 55 | -------------------------------------------------------------------------------- /test/undici-custom-dispatcher.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const Fastify = require('fastify') 5 | const { request, Pool } = require('undici') 6 | const From = require('..') 7 | 8 | class CustomDispatcher { 9 | constructor (...args) { 10 | this._dispatcher = new Pool(...args) 11 | } 12 | 13 | request (...args) { 14 | return this._dispatcher.request(...args) 15 | } 16 | 17 | close (...args) { 18 | return this._dispatcher.close(...args) 19 | } 20 | 21 | destroy (...args) { 22 | return this._dispatcher.destroy(...args) 23 | } 24 | } 25 | 26 | test('use a custom instance of \'undici\'', async t => { 27 | const target = Fastify({ 28 | keepAliveTimeout: 1 29 | }) 30 | 31 | target.get('/', (_req, reply) => { 32 | t.assert.ok('request proxied') 33 | 34 | reply.headers({ 35 | 'Content-Type': 'text/plain', 36 | 'x-my-header': 'hello!' 37 | }) 38 | 39 | reply.statusCode = 205 40 | reply.send('hello world') 41 | }) 42 | 43 | await target.listen({ port: 3001 }) 44 | t.after(async () => { 45 | await target.close() 46 | }) 47 | 48 | const instance = Fastify({ 49 | keepAliveTimeout: 1 50 | }) 51 | 52 | instance.register(From, { 53 | undici: new CustomDispatcher('http://localhost:3001') 54 | }) 55 | 56 | instance.get('/', (_request, reply) => { 57 | reply.from('http://myserver.local') 58 | }) 59 | 60 | await instance.listen({ port: 0 }) 61 | t.after(async () => { 62 | await instance.close() 63 | }) 64 | 65 | const res = await request(`http://localhost:${instance.server.address().port}`) 66 | 67 | t.assert.strictEqual(res.headers['content-type'], 'text/plain') 68 | t.assert.strictEqual(res.headers['x-my-header'], 'hello!') 69 | t.assert.strictEqual(res.statusCode, 205) 70 | 71 | const data = await res.body.text() 72 | t.assert.strictEqual(data, 'hello world') 73 | }) 74 | -------------------------------------------------------------------------------- /test/undici-global-agent.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const Fastify = require('fastify') 5 | const { request } = require('undici') 6 | const http = require('node:http') 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.after(() => instance.close()) 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.after(() => target.close()) 30 | 31 | await new Promise(resolve => target.listen({ port: 0 }, resolve)) 32 | 33 | instance.register(From, { 34 | base: `http://localhost:${target.address().port}`, 35 | globalAgent: true 36 | }) 37 | 38 | await new Promise(resolve => instance.listen({ port: 0 }, resolve)) 39 | 40 | const result = await request(`http://localhost:${instance.server.address().port}`) 41 | t.assert.strictEqual(result.statusCode, 200) 42 | 43 | const result1 = await request(`http://localhost:${instance.server.address().port}`) 44 | t.assert.strictEqual(result1.statusCode, 200) 45 | 46 | target.close() 47 | }) 48 | -------------------------------------------------------------------------------- /test/undici-no-destroy.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const Fastify = require('fastify') 5 | const undici = require('undici') 6 | const From = require('..') 7 | 8 | test('destroyAgent false', async (t) => { 9 | const mockAgent = new undici.Agent() 10 | mockAgent.destroy = () => { 11 | t.fail() 12 | } 13 | const instance = Fastify() 14 | 15 | t.after(() => instance.close()) 16 | 17 | instance.get('/', (_request, reply) => { 18 | reply.from() 19 | }) 20 | 21 | instance.register(From, { 22 | base: 'http://localhost:4242', 23 | undici: mockAgent, 24 | destroyAgent: false 25 | }) 26 | 27 | await instance.ready() 28 | await instance.close() 29 | }) 30 | -------------------------------------------------------------------------------- /test/undici-options.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const t = require('node:test') 4 | const assert = require('node:assert') 5 | const Fastify = require('fastify') 6 | const { request } = require('undici') 7 | const proxyquire = require('proxyquire') 8 | const http = require('node:http') 9 | const undici = require('undici') 10 | const { getUndiciOptions } = require('../lib/request') 11 | 12 | const instance = Fastify() 13 | 14 | t.test('undici options', async (t) => { 15 | t.plan(2) 16 | t.after(() => instance.close()) 17 | 18 | const target = http.createServer((_req, res) => { 19 | res.statusCode = 200 20 | res.end('hello world') 21 | }) 22 | 23 | instance.get('/', (_request, reply) => { 24 | reply.from() 25 | }) 26 | 27 | t.after(() => target.close()) 28 | 29 | await new Promise(resolve => target.listen({ port: 0 }, resolve)) 30 | 31 | const From = proxyquire('..', { 32 | './lib/request.js': proxyquire('../lib/request.js', { 33 | undici: undiciProxy 34 | }) 35 | }) 36 | 37 | instance.register(From, { 38 | base: `http://localhost:${target.address().port}`, 39 | undici: buildUndiciOptions() 40 | }) 41 | 42 | await new Promise(resolve => instance.listen({ port: 0 }, resolve)) 43 | 44 | const result = await request(`http://localhost:${instance.server.address().port}`) 45 | t.assert.strictEqual(result.statusCode, 200) 46 | t.assert.strictEqual(await result.body.text(), 'hello world') 47 | }) 48 | 49 | function undiciProxy () {} 50 | undiciProxy.Agent = class Agent extends undici.Agent { 51 | constructor (opts) { 52 | super(opts) 53 | assert.deepStrictEqual(opts, buildUndiciOptions()) 54 | } 55 | } 56 | undiciProxy.Pool = class Pool extends undici.Pool { 57 | constructor (url, options) { 58 | super(url, options) 59 | assert.deepStrictEqual(options, buildUndiciOptions()) 60 | } 61 | } 62 | 63 | function buildUndiciOptions () { 64 | return getUndiciOptions({ 65 | connections: 42, 66 | pipelining: 24, 67 | keepAliveTimeout: 4242, 68 | strictContentLength: false 69 | }) 70 | } 71 | -------------------------------------------------------------------------------- /test/undici-proxy-agent.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test, after } = require('node:test') 4 | const { createServer } = require('node:http') 5 | const Fastify = require('fastify') 6 | const { request } = require('undici') 7 | const { createProxy } = require('proxy') 8 | const fastifyProxyFrom = require('..') 9 | const { isIPv6 } = require('node:net') 10 | 11 | const configFormat = { 12 | string: (value) => value, 13 | 'url instance': (value) => new URL(value), 14 | object: (value) => ({ uri: value }) 15 | } 16 | 17 | for (const [description, format] of Object.entries(configFormat)) { 18 | test(`use undici ProxyAgent to connect through proxy - configured via ${description}`, async (t) => { 19 | t.plan(3) 20 | 21 | const target = await buildServer() 22 | const proxy = await buildProxy() 23 | 24 | after(() => { 25 | target.close() 26 | proxy.close() 27 | }) 28 | 29 | let targetAddress = target.address().address 30 | 31 | if (isIPv6(targetAddress)) { 32 | targetAddress = `[${targetAddress}]` 33 | } 34 | 35 | let proxyAddress = proxy.address().address 36 | 37 | if (isIPv6(proxyAddress)) { 38 | proxyAddress = `[${proxyAddress}]` 39 | } 40 | 41 | const targetUrl = `http://${targetAddress}:${target.address().port}` 42 | const proxyUrl = `http://${proxyAddress}:${proxy.address().port}` 43 | 44 | proxy.on('connect', () => { 45 | t.assert.ok(true, 'should connect to proxy') 46 | }) 47 | 48 | target.on('request', (_req, res) => { 49 | res.setHeader('content-type', 'application/json') 50 | res.end(JSON.stringify({ hello: 'world' })) 51 | }) 52 | 53 | const instance = Fastify() 54 | 55 | after(() => { 56 | instance.close() 57 | }) 58 | 59 | instance.register(fastifyProxyFrom, { 60 | base: targetUrl, 61 | undici: { 62 | proxy: format(proxyUrl) 63 | } 64 | }) 65 | 66 | instance.get('/', (_request, reply) => { 67 | reply.from() 68 | }) 69 | 70 | await instance.listen({ port: 0 }) 71 | 72 | let instanceAddress = proxy.address().address 73 | 74 | if (isIPv6(instanceAddress)) { 75 | if (instanceAddress === '::') { 76 | instanceAddress = '::1' 77 | } else { 78 | instanceAddress = `[${instanceAddress}]` 79 | } 80 | } 81 | 82 | const response = await request(`http://localhost:${instance.server.address().port}`) 83 | 84 | t.assert.strictEqual(response.statusCode, 200) 85 | t.assert.deepStrictEqual(await response.body.json(), { hello: 'world' }) 86 | }) 87 | } 88 | 89 | function buildServer () { 90 | return new Promise((resolve) => { 91 | const server = createServer() 92 | server.listen(0, () => resolve(server)) 93 | }) 94 | } 95 | 96 | function buildProxy () { 97 | return new Promise((resolve) => { 98 | const server = createProxy(createServer()) 99 | server.listen(0, () => resolve(server)) 100 | }) 101 | } 102 | -------------------------------------------------------------------------------- /test/undici-retry.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Fastify = require('fastify') 4 | const { request, Agent } = require('undici') 5 | const From = require('..') 6 | const { test } = require('node:test') 7 | 8 | let retryNum = 1 9 | 10 | const target = require('node:http').createServer(function (req, res) { 11 | if (retryNum % 2 !== 0) { 12 | req.socket.destroy() 13 | } else { 14 | res.statusCode = 200 15 | res.setHeader('Content-Type', 'text/plain') 16 | res.end('hello world') 17 | } 18 | 19 | retryNum += 1 20 | }) 21 | 22 | test('Will retry', async function (t) { 23 | t.after(() => { retryNum = 1 }) 24 | 25 | await new Promise(resolve => target.listen({ port: 0 }, resolve)) 26 | t.after(() => target.close()) 27 | 28 | const instance = Fastify() 29 | 30 | instance.register(From, { undici: true }) 31 | 32 | instance.get('/', (_request, reply) => { 33 | reply.from(`http://localhost:${target.address().port}/`, { 34 | retriesCount: 1, 35 | onError: (reply, { error }) => { 36 | t.assert.strictEqual(error.code, 'UND_ERR_SOCKET') 37 | reply.send(error) 38 | } 39 | }) 40 | }) 41 | 42 | await instance.listen({ port: 0 }) 43 | t.after(() => instance.close()) 44 | 45 | const { statusCode } = await request(`http://localhost:${instance.server.address().port}/`, { dispatcher: new Agent({ pipelining: 0 }) }) 46 | t.assert.strictEqual(statusCode, 200) 47 | }) 48 | 49 | test('will not retry', async function (t) { 50 | t.after(() => { retryNum = 1 }) 51 | 52 | await new Promise(resolve => target.listen({ port: 0 }, resolve)) 53 | t.after(() => target.close()) 54 | 55 | const instance = Fastify() 56 | 57 | instance.register(From, { undici: true }) 58 | 59 | instance.get('/', (_request, reply) => { 60 | reply.from(`http://localhost:${target.address().port}/`, { 61 | retriesCount: 0, 62 | onError: (reply, { error }) => { 63 | t.assert.strictEqual(error.code, 'UND_ERR_SOCKET') 64 | reply.send(error) 65 | } 66 | }) 67 | }) 68 | 69 | await instance.listen({ port: 0 }) 70 | t.after(() => instance.close()) 71 | 72 | const result = await request(`http://localhost:${instance.server.address().port}/`, { dispatcher: new Agent({ pipelining: 0 }) }) 73 | 74 | t.assert.strictEqual(result.statusCode, 500) 75 | }) 76 | 77 | test('will not retry unsupported method', async function (t) { 78 | t.after(() => { retryNum = 1 }) 79 | 80 | await new Promise(resolve => target.listen({ port: 0 }, resolve)) 81 | t.after(() => target.close()) 82 | 83 | const instance = Fastify() 84 | 85 | instance.register(From, { undici: true, retryMethods: ['DELETE'] }) 86 | 87 | instance.get('/', (_request, reply) => { 88 | reply.from(`http://localhost:${target.address().port}/`, { 89 | retriesCount: 1, 90 | onError: (reply, { error }) => { 91 | t.assert.strictEqual(error.code, 'UND_ERR_SOCKET') 92 | reply.send(error) 93 | } 94 | }) 95 | }) 96 | 97 | await instance.listen({ port: 0 }) 98 | t.after(() => instance.close()) 99 | 100 | const result = await request(`http://localhost:${instance.server.address().port}/`, { dispatcher: new Agent({ pipelining: 0 }) }) 101 | t.assert.strictEqual(result.statusCode, 500) 102 | }) 103 | -------------------------------------------------------------------------------- /test/undici-timeout-body-partial.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const t = require('node:test') 4 | const http = require('node:http') 5 | const Fastify = require('fastify') 6 | const { request, Agent } = require('undici') 7 | const From = require('..') 8 | const FakeTimers = require('@sinonjs/fake-timers') 9 | 10 | const clock = FakeTimers.createClock() 11 | 12 | t.test('undici body timeout', async (t) => { 13 | const target = http.createServer((req, res) => { 14 | t.assert.ok('request proxied') 15 | req.on('data', () => undefined) 16 | req.on('end', () => { 17 | res.writeHead(200) 18 | res.flushHeaders() 19 | res.write('test') 20 | clock.setTimeout(() => { 21 | res.end() 22 | }, 1000) 23 | }) 24 | }) 25 | 26 | await new Promise(resolve => target.listen({ port: 0 }, resolve)) 27 | 28 | const instance = Fastify() 29 | t.after(() => instance.close()) 30 | t.after(() => target.close()) 31 | 32 | instance.register(From, { 33 | base: `http://localhost:${target.address().port}`, 34 | undici: { 35 | bodyTimeout: 100 36 | } 37 | }) 38 | 39 | instance.get('/', (_request, reply) => { 40 | reply.from() 41 | }) 42 | 43 | await instance.listen({ port: 0 }) 44 | 45 | const result = await request(`http://localhost:${instance.server.address().port}/`, { 46 | dispatcher: new Agent({ 47 | pipelining: 0 48 | }) 49 | }) 50 | 51 | t.assert.strictEqual(result.statusCode, 200) 52 | 53 | clock.tick(1000) 54 | }) 55 | -------------------------------------------------------------------------------- /test/undici-timeout-body.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const t = require('node:test') 4 | const http = require('node:http') 5 | const Fastify = require('fastify') 6 | const { request, Agent } = require('undici') 7 | const From = require('..') 8 | const FakeTimers = require('@sinonjs/fake-timers') 9 | 10 | const clock = FakeTimers.createClock() 11 | 12 | t.test('undici body timeout', async (t) => { 13 | const target = http.createServer((req, res) => { 14 | t.assert.ok('request proxied') 15 | req.on('data', () => undefined) 16 | req.on('end', () => { 17 | res.flushHeaders() 18 | clock.setTimeout(() => { 19 | res.end() 20 | }, 1000) 21 | }) 22 | }) 23 | 24 | await target.listen({ port: 0 }) 25 | 26 | const instance = Fastify() 27 | t.after(() => instance.close()) 28 | t.after(() => target.close()) 29 | 30 | instance.register(From, { 31 | base: `http://localhost:${target.address().port}`, 32 | undici: { 33 | bodyTimeout: 100 34 | } 35 | }) 36 | 37 | instance.get('/', (_request, reply) => { 38 | reply.from() 39 | }) 40 | 41 | await instance.listen({ port: 0 }) 42 | 43 | const result = await request(`http://localhost:${instance.server.address().port}`, { 44 | dispatcher: new Agent({ 45 | pipelining: 0 46 | }) 47 | }) 48 | 49 | t.assert.strictEqual(result.statusCode, 500) 50 | t.assert.deepStrictEqual(await result.body.json(), { 51 | statusCode: 500, 52 | code: 'UND_ERR_BODY_TIMEOUT', 53 | error: 'Internal Server Error', 54 | message: 'Body Timeout Error' 55 | }) 56 | clock.tick(1000) 57 | }) 58 | -------------------------------------------------------------------------------- /test/undici-timeout.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const Fastify = require('fastify') 5 | const { request, Agent } = require('undici') 6 | const From = require('..') 7 | const FakeTimers = require('@sinonjs/fake-timers') 8 | 9 | test('undici request timeout', async (t) => { 10 | const clock = FakeTimers.createClock() 11 | const target = Fastify() 12 | t.after(() => target.close()) 13 | 14 | target.get('/', (_request, reply) => { 15 | t.assert.ok('request arrives') 16 | 17 | setTimeout(() => { 18 | reply.status(200).send('hello world') 19 | }, 1000) 20 | 21 | clock.tick(1000) 22 | }) 23 | 24 | await target.listen({ port: 0 }) 25 | 26 | const instance = Fastify() 27 | t.after(() => instance.close()) 28 | 29 | instance.register(From, { 30 | base: `http://localhost:${target.server.address().port}`, 31 | undici: { 32 | headersTimeout: 100 33 | } 34 | }) 35 | 36 | instance.get('/', (_request, reply) => { 37 | reply.from() 38 | }) 39 | 40 | await instance.listen({ port: 0 }) 41 | 42 | const result = await request(`http://localhost:${instance.server.address().port}`, { 43 | dispatcher: new Agent({ 44 | pipelining: 0 45 | }) 46 | }) 47 | 48 | t.assert.strictEqual(result.statusCode, 504) 49 | t.assert.match(result.headers['content-type'], /application\/json/) 50 | t.assert.deepStrictEqual(await result.body.json(), { 51 | statusCode: 504, 52 | code: 'FST_REPLY_FROM_GATEWAY_TIMEOUT', 53 | error: 'Gateway Timeout', 54 | message: 'Gateway Timeout' 55 | }) 56 | clock.tick(1000) 57 | }) 58 | 59 | test('undici request with specific timeout', async (t) => { 60 | const clock = FakeTimers.createClock() 61 | const target = Fastify() 62 | t.after(() => target.close()) 63 | 64 | target.get('/', (_request, reply) => { 65 | t.assert.ok('request arrives') 66 | 67 | setTimeout(() => { 68 | reply.status(200).send('hello world') 69 | }, 1000) 70 | 71 | clock.tick(1000) 72 | }) 73 | 74 | await target.listen({ port: 0 }) 75 | 76 | const instance = Fastify() 77 | t.after(() => instance.close()) 78 | 79 | instance.register(From, { 80 | base: `http://localhost:${target.server.address().port}`, 81 | undici: { 82 | headersTimeout: 100, 83 | } 84 | }) 85 | 86 | instance.get('/success', (_request, reply) => { 87 | reply.from('/', { 88 | timeout: 1000 89 | }) 90 | }) 91 | instance.get('/fail', (_request, reply) => { 92 | reply.from('/', { 93 | timeout: 50 94 | }) 95 | }) 96 | 97 | await instance.listen({ port: 0 }) 98 | 99 | const result = await request(`http://localhost:${instance.server.address().port}/success`, { 100 | dispatcher: new Agent({ 101 | pipelining: 0 102 | }) 103 | }) 104 | t.assert.strictEqual(result.statusCode, 200) 105 | 106 | const result2 = await request(`http://localhost:${instance.server.address().port}/fail`, { 107 | dispatcher: new Agent({ 108 | pipelining: 0 109 | }) 110 | }) 111 | 112 | t.assert.strictEqual(result2.statusCode, 504) 113 | t.assert.match(result2.headers['content-type'], /application\/json/) 114 | t.assert.deepStrictEqual(await result2.body.json(), { 115 | statusCode: 504, 116 | code: 'FST_REPLY_FROM_GATEWAY_TIMEOUT', 117 | error: 'Gateway Timeout', 118 | message: 'Gateway Timeout' 119 | }) 120 | clock.tick(1000) 121 | }) 122 | -------------------------------------------------------------------------------- /test/undici-with-path-in-base.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const t = require('node:test') 4 | const Fastify = require('fastify') 5 | const { request } = require('undici') 6 | const From = require('..') 7 | const http = require('node:http') 8 | 9 | const instance = Fastify() 10 | 11 | t.test('undici request with path in base', async (t) => { 12 | t.plan(8) 13 | t.after(() => instance.close()) 14 | 15 | const target = http.createServer((req, res) => { 16 | t.assert.ok('request proxied') 17 | t.assert.strictEqual(req.method, 'GET') 18 | t.assert.strictEqual(req.url, '/hello') 19 | t.assert.strictEqual(req.headers.connection, 'keep-alive') 20 | res.statusCode = 205 21 | res.setHeader('Content-Type', 'text/plain') 22 | res.setHeader('x-my-header', 'hello!') 23 | res.end('hello world') 24 | }) 25 | 26 | instance.get('/', (_request, reply) => { 27 | reply.from('/hello') 28 | }) 29 | 30 | t.after(() => target.close()) 31 | 32 | await new Promise((resolve) => target.listen({ port: 0 }, resolve)) 33 | 34 | instance.register(From, { 35 | base: `http://localhost:${target.address().port}/hello`, 36 | undici: true 37 | }) 38 | 39 | await new Promise((resolve) => instance.listen({ port: 0 }, resolve)) 40 | 41 | const result = await request(`http://localhost:${instance.server.address().port}`) 42 | 43 | t.assert.strictEqual(result.headers['content-type'], 'text/plain') 44 | t.assert.strictEqual(result.headers['x-my-header'], 'hello!') 45 | t.assert.strictEqual(result.statusCode, 205) 46 | t.assert.strictEqual(await result.body.text(), 'hello world') 47 | }) 48 | -------------------------------------------------------------------------------- /test/undici.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const t = require('node:test') 4 | const Fastify = require('fastify') 5 | const { request } = require('undici') 6 | const From = require('..') 7 | const http = require('node:http') 8 | 9 | const instance = Fastify() 10 | 11 | t.test('undici', async (t) => { 12 | t.plan(8) 13 | t.after(() => instance.close()) 14 | 15 | const target = http.createServer((req, res) => { 16 | t.assert.ok('request proxied') 17 | t.assert.strictEqual(req.method, 'GET') 18 | t.assert.strictEqual(req.url, '/') 19 | t.assert.strictEqual(req.headers.connection, 'keep-alive') 20 | res.statusCode = 205 21 | res.setHeader('Content-Type', 'text/plain') 22 | res.setHeader('x-my-header', 'hello!') 23 | res.end('hello world') 24 | }) 25 | 26 | instance.get('/', (_request, reply) => { 27 | reply.from() 28 | }) 29 | 30 | t.after(() => target.close()) 31 | 32 | await new Promise(resolve => target.listen({ port: 0 }, resolve)) 33 | 34 | instance.register(From, { 35 | base: `http://localhost:${target.address().port}`, 36 | undici: true 37 | }) 38 | 39 | await new Promise(resolve => instance.listen({ port: 0 }, resolve)) 40 | 41 | const result = await request(`http://localhost:${instance.server.address().port}`) 42 | 43 | t.assert.strictEqual(result.headers['content-type'], 'text/plain') 44 | t.assert.strictEqual(result.headers['x-my-header'], 'hello!') 45 | t.assert.strictEqual(result.statusCode, 205) 46 | t.assert.strictEqual(await result.body.text(), 'hello world') 47 | }) 48 | -------------------------------------------------------------------------------- /test/unexpected-error.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const Fastify = require('fastify') 5 | const { request } = require('undici') 6 | const proxyquire = require('proxyquire') 7 | 8 | // Stub request to throw error 'foo' 9 | const From = proxyquire('..', { 10 | './lib/request': function () { 11 | return { 12 | request: (_opts, callback) => { callback(new Error('foo')) }, 13 | close: () => {} 14 | } 15 | } 16 | }) 17 | 18 | test('unexpected error renders 500', async (t) => { 19 | const instance = Fastify() 20 | 21 | t.after(() => instance.close()) 22 | 23 | instance.get('/', (_request, reply) => { 24 | reply.code(205) 25 | reply.from() 26 | }) 27 | instance.register(From, { 28 | base: 'http://localhost' 29 | }) 30 | 31 | await instance.listen({ port: 0 }) 32 | 33 | const result = await request(`http://localhost:${instance.server.address().port}`) 34 | t.assert.strictEqual(result.statusCode, 500) 35 | t.assert.match(result.headers['content-type'], /application\/json/) 36 | t.assert.deepStrictEqual(await result.body.json(), { 37 | statusCode: 500, 38 | code: 'FST_REPLY_FROM_INTERNAL_SERVER_ERROR', 39 | error: 'Internal Server Error', 40 | message: 'foo' 41 | }) 42 | }) 43 | -------------------------------------------------------------------------------- /test/unix-http-undici-from.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const t = require('node:test') 4 | const Fastify = require('fastify') 5 | const { request } = require('undici') 6 | const From = require('..') 7 | const fs = require('node:fs') 8 | const querystring = require('node:querystring') 9 | const http = require('node:http') 10 | 11 | const instance = Fastify() 12 | instance.register(From) 13 | 14 | t.test('unix http undici from', { skip: process.platform === 'win32' }, async (t) => { 15 | t.plan(1) 16 | t.after(() => instance.close()) 17 | 18 | const socketPath = `${__filename}.socket` 19 | 20 | try { 21 | fs.unlinkSync(socketPath) 22 | } catch (_) { 23 | } 24 | 25 | const target = http.createServer((_req, res) => { 26 | t.fail('no response') 27 | res.end() 28 | }) 29 | 30 | instance.get('/', (_request, reply) => { 31 | reply.from(`unix+http://${querystring.escape(socketPath)}/hello`) 32 | }) 33 | 34 | t.after(() => target.close()) 35 | 36 | await instance.listen({ port: 0 }) 37 | 38 | await new Promise(resolve => target.listen(socketPath, resolve)) 39 | 40 | const result = await request(`http://localhost:${instance.server.address().port}`) 41 | t.assert.strictEqual(result.statusCode, 500) 42 | }) 43 | -------------------------------------------------------------------------------- /test/unix-http-undici.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const t = require('node:test') 4 | const Fastify = require('fastify') 5 | const { request } = require('undici') 6 | const From = require('..') 7 | const fs = require('node:fs') 8 | const querystring = require('node:querystring') 9 | const http = require('node:http') 10 | 11 | const socketPath = `${__filename}.socket` 12 | const upstream = `unix+http://${querystring.escape(socketPath)}/` 13 | 14 | const instance = Fastify() 15 | instance.register(From, { 16 | base: upstream 17 | }) 18 | 19 | t.test('unix http undici', { skip: process.platform === 'win32' }, async t => { 20 | t.plan(7) 21 | t.after(() => instance.close()) 22 | 23 | try { 24 | fs.unlinkSync(socketPath) 25 | } catch (_) { 26 | } 27 | 28 | const target = http.createServer((req, res) => { 29 | t.assert.ok('request proxied') 30 | t.assert.strictEqual(req.method, 'GET') 31 | t.assert.strictEqual(req.url, '/hello') 32 | res.statusCode = 205 33 | res.setHeader('Content-Type', 'text/plain') 34 | res.setHeader('x-my-header', 'hello!') 35 | res.end('hello world') 36 | }) 37 | 38 | instance.get('/', (_request, reply) => { 39 | reply.from('hello') 40 | }) 41 | 42 | t.after(() => target.close()) 43 | 44 | await instance.listen({ port: 0 }) 45 | 46 | await new Promise(resolve => target.listen(socketPath, resolve)) 47 | 48 | const result = await request(`http://localhost:${instance.server.address().port}`) 49 | 50 | t.assert.strictEqual(result.headers['content-type'], 'text/plain') 51 | t.assert.strictEqual(result.headers['x-my-header'], 'hello!') 52 | t.assert.strictEqual(result.statusCode, 205) 53 | t.assert.strictEqual(await result.body.text(), 'hello world') 54 | }) 55 | -------------------------------------------------------------------------------- /test/unix-http.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const t = require('node:test') 4 | const Fastify = require('fastify') 5 | const { request } = require('undici') 6 | const From = require('..') 7 | const fs = require('node:fs') 8 | const querystring = require('node:querystring') 9 | const http = require('node:http') 10 | 11 | const socketPath = `${__filename}.socket` 12 | const upstream = `unix+http://${querystring.escape(socketPath)}/` 13 | 14 | const instance = Fastify() 15 | instance.register(From, { 16 | // Use node core http, unix sockets are not 17 | // supported yet. 18 | http: true, 19 | base: upstream 20 | }) 21 | 22 | t.test('unix http', { skip: process.platform === 'win32' }, async (t) => { 23 | t.plan(7) 24 | t.after(() => instance.close()) 25 | 26 | try { 27 | fs.unlinkSync(socketPath) 28 | } catch (_) { 29 | } 30 | 31 | const target = http.createServer((req, res) => { 32 | t.assert.ok('request proxied') 33 | t.assert.strictEqual(req.method, 'GET') 34 | t.assert.strictEqual(req.url, '/hello') 35 | res.statusCode = 205 36 | res.setHeader('Content-Type', 'text/plain') 37 | res.setHeader('x-my-header', 'hello!') 38 | res.end('hello world') 39 | }) 40 | 41 | instance.get('/', (_request, reply) => { 42 | reply.from('hello') 43 | }) 44 | 45 | t.after(() => target.close()) 46 | 47 | await instance.listen({ port: 0 }) 48 | 49 | await new Promise(resolve => target.listen(socketPath, resolve)) 50 | 51 | const result = await request(`http://localhost:${instance.server.address().port}`) 52 | 53 | t.assert.strictEqual(result.headers['content-type'], 'text/plain') 54 | t.assert.strictEqual(result.headers['x-my-header'], 'hello!') 55 | t.assert.strictEqual(result.statusCode, 205) 56 | t.assert.strictEqual(await result.body.text(), 'hello world') 57 | }) 58 | -------------------------------------------------------------------------------- /test/unix-https-undici.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const t = require('node:test') 4 | const Fastify = require('fastify') 5 | const From = require('..') 6 | const https = require('node:https') 7 | const fs = require('node:fs') 8 | const { request, Agent } = require('undici') 9 | const querystring = require('node:querystring') 10 | const path = require('node: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 | const socketPath = `${__filename}.socket` 17 | 18 | try { 19 | fs.unlinkSync(socketPath) 20 | } catch (_) { 21 | } 22 | 23 | const instance = Fastify({ 24 | https: certs 25 | }) 26 | instance.register(From, { 27 | base: `unix+https://${querystring.escape(socketPath)}` 28 | }) 29 | 30 | t.test('unix https undici', { skip: process.platform === 'win32' }, async (t) => { 31 | t.plan(7) 32 | t.after(() => instance.close()) 33 | 34 | const target = https.createServer(certs, (req, res) => { 35 | t.assert.ok('request proxied') 36 | t.assert.strictEqual(req.method, 'GET') 37 | t.assert.strictEqual(req.url, '/hello') 38 | res.statusCode = 205 39 | res.setHeader('Content-Type', 'text/plain') 40 | res.setHeader('x-my-header', 'hello!') 41 | res.end('hello world') 42 | }) 43 | 44 | instance.get('/', (_request, reply) => { 45 | reply.from('hello') 46 | }) 47 | 48 | t.after(() => target.close()) 49 | 50 | await instance.listen({ port: 0 }) 51 | 52 | await new Promise(resolve => target.listen(socketPath, resolve)) 53 | 54 | const result = await request(`https://localhost:${instance.server.address().port}`, { 55 | dispatcher: new Agent({ 56 | connect: { 57 | rejectUnauthorized: false 58 | } 59 | }) 60 | }) 61 | 62 | t.assert.strictEqual(result.headers['content-type'], 'text/plain') 63 | t.assert.strictEqual(result.headers['x-my-header'], 'hello!') 64 | t.assert.strictEqual(result.statusCode, 205) 65 | t.assert.strictEqual(await result.body.text(), 'hello world') 66 | }) 67 | -------------------------------------------------------------------------------- /test/unix-https.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const t = require('node:test') 4 | const Fastify = require('fastify') 5 | const From = require('..') 6 | const https = require('node:https') 7 | const { request, Agent } = require('undici') 8 | const fs = require('node:fs') 9 | const querystring = require('node:querystring') 10 | const path = require('node: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 | const instance = Fastify({ 17 | https: certs 18 | }) 19 | instance.register(From, { 20 | http: true 21 | }) 22 | 23 | t.test('unix https', { skip: process.platform === 'win32' }, async (t) => { 24 | t.plan(7) 25 | t.after(() => instance.close()) 26 | 27 | const socketPath = `${__filename}.socket` 28 | 29 | try { 30 | fs.unlinkSync(socketPath) 31 | } catch (_) { 32 | } 33 | 34 | const target = https.createServer(certs, (req, res) => { 35 | t.assert.ok('request proxied') 36 | t.assert.strictEqual(req.method, 'GET') 37 | t.assert.strictEqual(req.url, '/hello') 38 | res.statusCode = 205 39 | res.setHeader('Content-Type', 'text/plain') 40 | res.setHeader('x-my-header', 'hello!') 41 | res.end('hello world') 42 | }) 43 | 44 | instance.get('/', (_request, reply) => { 45 | reply.from(`unix+https://${querystring.escape(socketPath)}/hello`) 46 | }) 47 | 48 | t.after(() => target.close()) 49 | 50 | await instance.listen({ port: 0 }) 51 | 52 | await new Promise(resolve => target.listen(socketPath, resolve)) 53 | 54 | const result = await request(`https://localhost:${instance.server.address().port}`, { 55 | dispatcher: new Agent({ 56 | connect: { 57 | rejectUnauthorized: false 58 | } 59 | }) 60 | }) 61 | 62 | t.assert.strictEqual(result.headers['content-type'], 'text/plain') 63 | t.assert.strictEqual(result.headers['x-my-header'], 'hello!') 64 | t.assert.strictEqual(result.statusCode, 205) 65 | t.assert.strictEqual(await result.body.text(), 'hello world') 66 | }) 67 | -------------------------------------------------------------------------------- /test/utils-filter-pseudo-headers.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tap').test 4 | const filterPseudoHeaders = require('../lib/utils').filterPseudoHeaders 5 | 6 | test('filterPseudoHeaders', t => { 7 | t.plan(1) 8 | const headers = { 9 | accept: '*/*', 10 | 'Content-Type': 'text/html; charset=UTF-8', 11 | ':method': 'GET' 12 | } 13 | 14 | t.strictSame(filterPseudoHeaders(headers), { 15 | accept: '*/*', 16 | 'content-type': 'text/html; charset=UTF-8' 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { 4 | FastifyPluginCallback, 5 | FastifyReply, 6 | FastifyRequest, 7 | HTTPMethods, 8 | RawReplyDefaultExpression, 9 | RawServerBase, 10 | RequestGenericInterface, 11 | RouteGenericInterface 12 | } from 'fastify' 13 | 14 | import { 15 | Agent, 16 | AgentOptions, 17 | IncomingHttpHeaders, 18 | RequestOptions, 19 | } from 'node:http' 20 | import { 21 | ClientSessionOptions, 22 | ClientSessionRequestOptions, 23 | IncomingHttpHeaders as Http2IncomingHttpHeaders, 24 | SecureClientSessionOptions, 25 | } from 'node:http2' 26 | import { 27 | Agent as SecureAgent, 28 | AgentOptions as SecureAgentOptions, 29 | RequestOptions as SecureRequestOptions 30 | } from 'node:https' 31 | import { Pool, ProxyAgent, Dispatcher } from 'undici' 32 | 33 | declare module 'fastify' { 34 | interface FastifyReply { 35 | from( 36 | source?: string, 37 | opts?: fastifyReplyFrom.FastifyReplyFromHooks 38 | ): this; 39 | } 40 | } 41 | 42 | type FastifyReplyFrom = FastifyPluginCallback 43 | declare namespace fastifyReplyFrom { 44 | type QueryStringFunction = ( 45 | search: string | undefined, 46 | reqUrl: string, 47 | request: FastifyRequest 48 | ) => string 49 | 50 | export type RetryDetails = { 51 | err: Error; 52 | req: FastifyRequest; 53 | res: FastifyReply; 54 | attempt: number; 55 | retriesCount: number; 56 | getDefaultDelay: () => number | null; 57 | } 58 | export interface FastifyReplyFromHooks { 59 | queryString?: { [key: string]: unknown } | QueryStringFunction; 60 | contentType?: string; 61 | retryDelay?: (details: RetryDetails) => {} | null; 62 | retriesCount?: number; 63 | onResponse?: ( 64 | request: FastifyRequest, 65 | reply: FastifyReply, 66 | res: RawReplyDefaultExpression 67 | ) => void; 68 | onError?: ( 69 | reply: FastifyReply, 70 | error: { error: Error } 71 | ) => void; 72 | body?: unknown; 73 | rewriteHeaders?: ( 74 | headers: Http2IncomingHttpHeaders | IncomingHttpHeaders, 75 | request?: FastifyRequest 76 | ) => Http2IncomingHttpHeaders | IncomingHttpHeaders; 77 | rewriteRequestHeaders?: ( 78 | request: FastifyRequest, 79 | headers: Http2IncomingHttpHeaders | IncomingHttpHeaders 80 | ) => Http2IncomingHttpHeaders | IncomingHttpHeaders; 81 | getUpstream?: ( 82 | request: FastifyRequest, 83 | base: string 84 | ) => string; 85 | method?: HTTPMethods; 86 | timeout?: number; 87 | } 88 | 89 | interface Http2Options { 90 | sessionTimeout?: number; 91 | requestTimeout?: number; 92 | sessionOptions?: ClientSessionOptions | SecureClientSessionOptions; 93 | requestOptions?: ClientSessionRequestOptions; 94 | } 95 | 96 | interface HttpOptions { 97 | agentOptions?: AgentOptions | SecureAgentOptions; 98 | requestOptions?: RequestOptions | SecureRequestOptions; 99 | agents?: { 'http:': Agent, 'https:': SecureAgent } 100 | } 101 | 102 | export interface FastifyReplyFromOptions { 103 | base?: string; 104 | cacheURLs?: number; 105 | disableCache?: boolean; 106 | http?: HttpOptions; 107 | http2?: Http2Options | boolean; 108 | undici?: Pool.Options & { proxy?: string | URL | ProxyAgent.Options } | { request: Dispatcher['request'] }; 109 | contentTypesToEncode?: string[]; 110 | retryMethods?: (HTTPMethods | 'TRACE')[]; 111 | maxRetriesOn503?: number; 112 | disableRequestLogging?: boolean; 113 | globalAgent?: boolean; 114 | destroyAgent?: boolean; 115 | } 116 | 117 | export const fastifyReplyFrom: FastifyReplyFrom 118 | export { fastifyReplyFrom as default } 119 | } 120 | 121 | declare function fastifyReplyFrom (...params: Parameters): ReturnType 122 | export = fastifyReplyFrom 123 | -------------------------------------------------------------------------------- /types/index.test-d.ts: -------------------------------------------------------------------------------- 1 | import fastify, { FastifyReply, FastifyRequest, RawReplyDefaultExpression, RawServerBase, RequestGenericInterface, RouteGenericInterface } from 'fastify' 2 | import * as http from 'node:http' 3 | import { IncomingHttpHeaders } from 'node:http2' 4 | import * as https from 'node:https' 5 | import { AddressInfo } from 'node:net' 6 | import { expectType } from 'tsd' 7 | import { Agent, Client, Dispatcher, Pool } from 'undici' 8 | import replyFrom, { FastifyReplyFromOptions } from '..' 9 | // @ts-ignore 10 | import tap from 'tap' 11 | 12 | const fullOptions: FastifyReplyFromOptions = { 13 | base: 'http://example2.com', 14 | http: { 15 | agentOptions: { 16 | keepAliveMsecs: 60 * 1000, 17 | maxFreeSockets: 2048, 18 | maxSockets: 2048 19 | }, 20 | requestOptions: { 21 | timeout: 1000 22 | }, 23 | agents: { 24 | 'http:': new http.Agent({}), 25 | 'https:': new https.Agent({}) 26 | } 27 | }, 28 | http2: { 29 | sessionTimeout: 1000, 30 | requestTimeout: 1000, 31 | sessionOptions: { 32 | rejectUnauthorized: true 33 | }, 34 | requestOptions: { 35 | endStream: true 36 | } 37 | }, 38 | cacheURLs: 100, 39 | disableCache: false, 40 | undici: { 41 | connections: 100, 42 | pipelining: 10, 43 | proxy: 'http://example2.com:8080' 44 | }, 45 | contentTypesToEncode: ['application/x-www-form-urlencoded'], 46 | retryMethods: ['GET', 'HEAD', 'OPTIONS', 'TRACE'], 47 | maxRetriesOn503: 10, 48 | disableRequestLogging: false, 49 | globalAgent: false, 50 | destroyAgent: true 51 | } 52 | 53 | async function main () { 54 | const server = fastify() 55 | 56 | server.register(replyFrom) 57 | 58 | server.register(replyFrom, {}) 59 | 60 | server.register(replyFrom, { http2: true }) 61 | 62 | server.register(replyFrom, fullOptions) 63 | 64 | server.register(replyFrom, { undici: { proxy: new URL('http://example2.com:8080') } }) 65 | 66 | server.register(replyFrom, { undici: { proxy: { uri: 'http://example2.com:8080' } } }) 67 | 68 | server.get('/v1', (_request, reply) => { 69 | expectType(reply.from()) 70 | }) 71 | server.get('/v3', (_request, reply) => { 72 | reply.from('/v3', { 73 | timeout: 1000, 74 | body: { hello: 'world' }, 75 | rewriteRequestHeaders (req, headers) { 76 | expectType>(req) 77 | return headers 78 | }, 79 | getUpstream (req, base) { 80 | expectType>(req) 81 | return base 82 | }, 83 | onResponse (request, reply, res) { 84 | expectType>(request) 85 | expectType>(reply) 86 | expectType>(res) 87 | expectType(res.statusCode) 88 | } 89 | }) 90 | }) 91 | 92 | // http2 93 | const instance = fastify({ http2: true }) 94 | // @ts-ignore 95 | tap.tearDown(instance.close.bind(instance)) 96 | const target = fastify({ http2: true }) 97 | // @ts-ignore 98 | tap.tearDown(target.close.bind(target)) 99 | instance.get('/', (_request, reply) => { 100 | reply.from() 101 | }) 102 | 103 | instance.get('/http2', (_request, reply) => { 104 | reply.from('/', { 105 | method: 'POST', 106 | retryDelay: ({ req, res, getDefaultDelay }) => { 107 | const defaultDelay = getDefaultDelay() 108 | if (defaultDelay) return defaultDelay 109 | 110 | if (res && res.statusCode === 500 && req.method === 'GET') { 111 | return 300 112 | } 113 | return null 114 | }, 115 | rewriteHeaders (headers) { 116 | return headers 117 | }, 118 | rewriteRequestHeaders (_req, headers: IncomingHttpHeaders) { 119 | return headers 120 | }, 121 | getUpstream (_req, base) { 122 | return base 123 | }, 124 | onError (reply: FastifyReply, error) { 125 | return reply.send(error.error) 126 | }, 127 | queryString (search, reqUrl, request) { 128 | expectType(search) 129 | expectType(reqUrl) 130 | expectType>(request) 131 | return '' 132 | }, 133 | }) 134 | }) 135 | 136 | await target.listen({ port: 0 }) 137 | const port = (target.server.address() as AddressInfo).port 138 | instance.register(replyFrom, { 139 | base: `http://localhost:${port}`, 140 | http2: { 141 | sessionOptions: { 142 | rejectUnauthorized: false, 143 | }, 144 | }, 145 | }) 146 | instance.register(replyFrom, { 147 | base: `http://localhost:${port}`, 148 | http2: true, 149 | }) 150 | await instance.listen({ port: 0 }) 151 | 152 | const undiciInstance = fastify() 153 | undiciInstance.register(replyFrom, { 154 | base: 'http://example2.com', 155 | undici: { 156 | pipelining: 10, 157 | connections: 10 158 | } 159 | }) 160 | await undiciInstance.ready() 161 | 162 | const undiciInstanceAgent = fastify() 163 | undiciInstance.register(replyFrom, { 164 | base: 'http://example2.com', 165 | undici: new Agent() 166 | }) 167 | await undiciInstanceAgent.ready() 168 | 169 | const undiciInstancePool = fastify() 170 | undiciInstance.register(replyFrom, { 171 | base: 'http://example2.com', 172 | undici: new Pool('http://example2.com') 173 | }) 174 | await undiciInstancePool.ready() 175 | 176 | const undiciInstanceClient = fastify() 177 | undiciInstance.register(replyFrom, { 178 | base: 'http://example2.com', 179 | undici: new Client('http://example2.com') 180 | }) 181 | await undiciInstanceClient.ready() 182 | 183 | const undiciInstanceDispatcher = fastify() 184 | undiciInstance.register(replyFrom, { 185 | base: 'http://example2.com', 186 | undici: new Dispatcher() 187 | }) 188 | await undiciInstanceDispatcher.ready() 189 | 190 | tap.pass('done') 191 | tap.end() 192 | } 193 | 194 | main() 195 | --------------------------------------------------------------------------------