├── test ├── fixtures │ ├── file.txt │ ├── fastify.cert │ └── fastify.key ├── http2-invalid-base.test.js ├── utils-filter-pseudo-headers.test.js ├── http2-unix-socket.test.js ├── undici-no-destroy.test.js ├── http-invalid-target.test.js ├── http2-invalid-target.test.js ├── fix-GHSA-2q7r-29rg-6m5h.test.js ├── base-path.test.js ├── unix-http-undici-from.test.js ├── get-upstream-undici.test.js ├── unexpected-error.test.js ├── rewrite-headers-type.test.js ├── http-global-agent.test.js ├── get-upstream-type.test.js ├── no-body-opts-with-get.test.js ├── rewrite-request-headers-type.test.js ├── onResponse.test.js ├── http2-target-crash.test.js ├── http-http2.test.js ├── undici-global-agent.test.js ├── on-invalid-upstream-response.test.js ├── full-get-test.test.js ├── undici-connect-timeout.test.js ├── base-get.test.js ├── full-querystring.test.js ├── full-querystring-rewrite.test.js ├── no-body-opts-with-head.test.js ├── undici-timeout-body-partial.test.js ├── base-querystring.test.js ├── full-delete-http2.test.js ├── full-querystring-rewrite-string.test.js ├── rewrite-request-headers.test.js ├── modifyCoreObjects-false.test.js ├── balanced-pool.test.js ├── undici.test.js ├── async-route-handler.test.js ├── full-querystring-rewrite-option.test.js ├── full-querystring-rewrite-option-complex.test.js ├── undici-chaining.test.js ├── no-stream-body-option.test.js ├── http2-te-header-not-stripped-if-trailing.test.js ├── undici-timeout-body.test.js ├── undici-with-path-in-base.test.js ├── rewrite-headers.test.js ├── http-agents.test.js ├── full-post-http2.test.js ├── core-with-path-in-base.test.js ├── full-post.test.js ├── full-querystring-rewrite-option-function.test.js ├── post-plain-text.test.js ├── http2-http2.test.js ├── unix-http-undici.test.js ├── http2-target-multi-crash.test.js ├── full-rewrite-body-to-null.test.js ├── get-upstream-cache.test.js ├── https-global-agent.test.js ├── unix-http.test.js ├── full-https-get.test.js ├── full-rewrite-body-to-empty-string.test.js ├── full-rewrite-body.test.js ├── transform-body.test.js ├── full-post-extended-content-type.test.js ├── full-querystring-rewrite-option-function-request.test.js ├── full-rewrite-body-http.test.js ├── padded-body.test.js ├── full-post-stream.test.js ├── full-querystring-url.test.js ├── undici-custom-dispatcher.test.js ├── full-post-stream-core.test.js ├── on-error.test.js ├── post-formbody.test.js ├── fix-GHSA-v2v2-hph8-q5xp.test.js ├── get-upstream-http.test.js ├── full-rewrite-body-content-type.test.js ├── undici-options.test.js ├── undici-body.test.js ├── unix-https-undici.test.js ├── unix-https.test.js ├── method.test.js ├── https-agents.test.js ├── post-with-octet-stream.test.js ├── post-with-custom-encoded-contenttype.test.js ├── text-event-stream-custom-parser.test.js ├── undici-agent.test.js ├── host-header.test.js ├── http2-https.test.js ├── custom-undici-instance.test.js ├── multipart-custom-parser.test.js ├── fastify-multipart-incompatibility.test.js ├── http2-timeout-disabled.test.js ├── undici-proxy-agent.test.js ├── http-retry.test.js ├── undici-retry.test.js ├── undici-timeout.test.js ├── retry-on-503.test.js ├── http2-canceled-streams-cleanup.test.js ├── build-url.test.js ├── http-timeout.test.js ├── http2-goaway.test.js ├── disable-request-logging.test.js └── http2-timeout.test.js ├── .npmrc ├── .taprc ├── .gitattributes ├── eslint.config.js ├── .github ├── dependabot.yml ├── workflows │ └── ci.yml └── stale.yml ├── examples └── example.js ├── LICENSE ├── lib ├── errors.js └── utils.js ├── package.json ├── .gitignore └── types └── index.d.ts /test/fixtures/file.txt: -------------------------------------------------------------------------------- 1 | file content -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | ignore-scripts=true 2 | package-lock=false 3 | -------------------------------------------------------------------------------- /.taprc: -------------------------------------------------------------------------------- 1 | disable-coverage: true 2 | files: 3 | - test/**/*.test.js 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set default behavior to automatically convert line endings 2 | * text=auto eol=lf 3 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = require('neostandard')({ 4 | ignores: require('neostandard').resolveIgnoresFromGitignore(), 5 | ts: true 6 | }) 7 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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/utils-filter-pseudo-headers.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('node: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.assert.deepStrictEqual(filterPseudoHeaders(headers), { 15 | accept: '*/*', 16 | 'content-type': 'text/html; charset=UTF-8' 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | # This allows a subsequently queued workflow run to interrupt previous runs 18 | concurrency: 19 | group: "${{ github.workflow }}-${{ github.event.pull_request.head.label || github.head_ref || github.ref }}" 20 | cancel-in-progress: true 21 | 22 | permissions: 23 | contents: read 24 | 25 | jobs: 26 | test: 27 | permissions: 28 | contents: write 29 | pull-requests: write 30 | uses: fastify/workflows/.github/workflows/plugins-ci.yml@v5 31 | with: 32 | license-check: true 33 | lint: true 34 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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/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/fix-GHSA-2q7r-29rg-6m5h.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('fix for GHSA-2q7r-29rg-6m5h vulnerability', async (t) => { 12 | t.plan(2) 13 | 14 | const target = http.createServer((_, res) => { 15 | res.statusCode = 205 16 | res.end('hi') 17 | }) 18 | await target.listen({ port: 0 }) 19 | t.after(() => target.close()) 20 | 21 | instance.get('/', (_request, reply) => { reply.from('/ho/%2E%2E/hi') }) 22 | instance.register(From, { 23 | base: `http://localhost:${target.address().port}/hi/`, 24 | undici: true 25 | }) 26 | await instance.listen({ port: 0 }) 27 | t.after(() => instance.close()) 28 | 29 | const { statusCode, body } = await request(`http://localhost:${instance.server.address().port}`) 30 | t.assert.strictEqual(statusCode, 400) 31 | t.assert.strictEqual(await body.text(), '{"statusCode":400,"error":"Bad Request","message":"source/request contain invalid characters"}') 32 | }) 33 | -------------------------------------------------------------------------------- /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/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/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/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/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 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-present Matteo Collina 4 | Copyright (c) 2017-present The Fastify team 5 | 6 | The Fastify team members are listed at https://github.com/fastify/fastify#team. 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in all 16 | copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/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/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/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/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/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-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/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/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/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/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-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/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/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/balanced-pool.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const t = require('node:test') 3 | const http = require('node:http') 4 | const Fastify = require('fastify') 5 | const From = require('..') 6 | const { request } = require('undici') 7 | 8 | t.test('undici balanced pool http', async t => { 9 | const hit = [0, 0] 10 | const makeTarget = idx => http.createServer((req, res) => { 11 | hit[idx]++ 12 | res.statusCode = 200 13 | res.end('hello world') 14 | }) 15 | const target1 = makeTarget(0) 16 | const target2 = makeTarget(1) 17 | 18 | await Promise.all([ 19 | new Promise(resolve => target1.listen(0, resolve)), 20 | new Promise(resolve => target2.listen(0, resolve)) 21 | ]) 22 | const p1 = target1.address().port 23 | const p2 = target2.address().port 24 | 25 | const proxy = Fastify() 26 | proxy.register(From, { 27 | base: [`http://localhost:${p1}`, `http://localhost:${p2}`] 28 | }) 29 | proxy.get('*', (_req, reply) => { 30 | reply.from() 31 | }) 32 | 33 | t.after(() => { 34 | proxy.close() 35 | target1.close() 36 | target2.close() 37 | }) 38 | 39 | await proxy.listen({ port: 0 }) 40 | const proxyPort = proxy.server.address().port 41 | 42 | for (let i = 0; i < 10; i++) { 43 | const res = await request(`http://localhost:${proxyPort}/hello`) 44 | t.assert.strictEqual(res.statusCode, 200) 45 | t.assert.strictEqual(await res.body.text(), 'hello world') 46 | } 47 | t.assert.ok(hit[0] > 0 && hit[1] > 0, `load distribution OK => [${hit[0]}, ${hit[1]}]`) 48 | }) 49 | -------------------------------------------------------------------------------- /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/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/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-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/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/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/http2-te-header-not-stripped-if-trailing.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const h2url = require('h2url') 3 | const t = require('node:test') 4 | const Fastify = require('fastify') 5 | const From = require('..') 6 | const fs = require('node:fs') 7 | const path = require('node:path') 8 | const certs = { 9 | key: fs.readFileSync(path.join(__dirname, 'fixtures', 'fastify.key')), 10 | cert: fs.readFileSync(path.join(__dirname, 'fixtures', 'fastify.cert')) 11 | } 12 | 13 | t.test('do not strip te header if set to trailing', async (t) => { 14 | const instance = Fastify({ 15 | http2: true, 16 | https: certs 17 | }) 18 | 19 | t.after(() => instance.close()) 20 | 21 | const target = Fastify({ 22 | http2: true 23 | }) 24 | 25 | target.get('/', (request, reply) => { 26 | t.assert.strictEqual(request.headers['te'], 'trailers') 27 | 28 | reply.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 } = await h2url.concat({ 50 | url: `https://localhost:${instance.server.address().port}`, 51 | headers: { 52 | te: 'trailers' 53 | } 54 | }) 55 | 56 | t.assert.strictEqual(headers[':status'], 200) 57 | 58 | instance.close() 59 | target.close() 60 | }) 61 | -------------------------------------------------------------------------------- /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-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/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/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/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/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/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-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/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/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/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/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/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/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/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/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/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/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-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.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/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/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-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-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/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/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-querystring-url.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 url', async (t) => { 12 | const target = http.createServer((req, res) => { 13 | t.assert.ok('request proxied') 14 | t.assert.strictEqual(req.method, 'GET') 15 | t.assert.strictEqual(req.url, '/hi?a=/ho/%2E%2E/hi') 16 | res.statusCode = 205 17 | res.setHeader('Content-Type', 'text/plain') 18 | res.setHeader('x-my-header', 'hi!') 19 | res.end('hi') 20 | }) 21 | 22 | await target.listen({ port: 0 }) 23 | t.after(() => target.close()) 24 | 25 | await instance.register(From, { 26 | base: `http://localhost:${target.address().port}` 27 | }) 28 | 29 | instance.get('/hi', (_request, reply) => { 30 | reply.from() 31 | }) 32 | 33 | instance.get('/foo', (_request, reply) => { 34 | reply.from('/hi') 35 | }) 36 | 37 | await instance.listen({ port: 0 }) 38 | t.after(() => instance.close()) 39 | 40 | { 41 | const result = await request(`http://localhost:${instance.server.address().port}/hi?a=/ho/%2E%2E/hi`) 42 | t.assert.strictEqual(result.headers['content-type'], 'text/plain') 43 | t.assert.strictEqual(result.headers['x-my-header'], 'hi!') 44 | t.assert.strictEqual(result.statusCode, 205) 45 | t.assert.strictEqual(await result.body.text(), 'hi') 46 | } 47 | 48 | { 49 | const result = await request(`http://localhost:${instance.server.address().port}/foo?a=/ho/%2E%2E/hi`) 50 | t.assert.strictEqual(result.headers['content-type'], 'text/plain') 51 | t.assert.strictEqual(result.headers['x-my-header'], 'hi!') 52 | t.assert.strictEqual(result.statusCode, 205) 53 | t.assert.strictEqual(await result.body.text(), 'hi') 54 | } 55 | }) 56 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/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/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-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/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/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/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/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/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/text-event-stream-custom-parser.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 | t.test('text/event-stream proxying with custom content type parser', async (t) => { 10 | t.plan(6) 11 | 12 | // Target server that sends SSE data 13 | const target = http.createServer((req, res) => { 14 | t.assert.ok('request proxied') 15 | t.assert.strictEqual(req.method, 'POST') 16 | t.assert.match(req.headers['content-type'], /^text\/event-stream/) 17 | 18 | let data = '' 19 | req.setEncoding('utf8') 20 | req.on('data', (chunk) => { 21 | data += chunk 22 | }) 23 | req.on('end', () => { 24 | // Verify the SSE data is received 25 | t.assert.match(data, /data: test message/) 26 | t.assert.match(data, /event: custom/) 27 | 28 | res.setHeader('content-type', 'application/json') 29 | res.statusCode = 200 30 | res.end(JSON.stringify({ received: 'sse data' })) 31 | }) 32 | }) 33 | 34 | // Fastify instance with custom text/event-stream parser 35 | const fastify = Fastify() 36 | 37 | // Register custom content type parser for text/event-stream 38 | // This allows the raw body to be passed through without parsing 39 | fastify.addContentTypeParser('text/event-stream', function (req, body, done) { 40 | done(null, body) 41 | }) 42 | 43 | fastify.register(From) 44 | 45 | fastify.post('/', (request, reply) => { 46 | reply.from(`http://localhost:${target.address().port}`) 47 | }) 48 | 49 | t.after(() => fastify.close()) 50 | t.after(() => target.close()) 51 | 52 | await fastify.listen({ port: 0 }) 53 | await target.listen({ port: 0 }) 54 | 55 | // Create SSE-like data 56 | const sseData = 'data: test message\nevent: custom\ndata: another line\n\n' 57 | 58 | // Send request with SSE data 59 | const result = await request(`http://localhost:${fastify.server.address().port}`, { 60 | method: 'POST', 61 | headers: { 62 | 'content-type': 'text/event-stream' 63 | }, 64 | body: sseData 65 | }) 66 | 67 | t.assert.deepStrictEqual(await result.body.json(), { received: 'sse data' }) 68 | }) 69 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@fastify/reply-from", 3 | "version": "12.5.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 --test-timeout=30000", 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 | "@sinonjs/fake-timers": "^15.0.0", 64 | "@types/node": "^24.0.8", 65 | "@types/tap": "^18.0.0", 66 | "c8": "^10.1.3", 67 | "eslint": "^9.17.0", 68 | "fastify": "^5.0.0", 69 | "form-data": "^4.0.0", 70 | "h2url": "^0.2.0", 71 | "neostandard": "^0.12.0", 72 | "nock": "^14.0.0", 73 | "proxy": "^2.1.1", 74 | "proxyquire": "^2.1.3", 75 | "split2": "^4.2.0", 76 | "tsd": "^0.33.0" 77 | }, 78 | "dependencies": { 79 | "@fastify/error": "^4.0.0", 80 | "end-of-stream": "^1.4.4", 81 | "fast-content-type-parse": "^3.0.0", 82 | "fast-querystring": "^1.1.2", 83 | "fastify-plugin": "^5.0.1", 84 | "toad-cache": "^3.7.0", 85 | "undici": "^7.0.0" 86 | }, 87 | "publishConfig": { 88 | "access": "public" 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /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/multipart-custom-parser.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 http = require('node:http') 10 | const FormData = require('form-data') 11 | 12 | t.test('multipart/form-data proxying with custom content type parser', async (t) => { 13 | t.plan(7) 14 | 15 | const filetPath = path.join(__dirname, 'fixtures', 'file.txt') 16 | 17 | // Target server that expects multipart/form-data 18 | const target = http.createServer((req, res) => { 19 | t.assert.ok('request proxied') 20 | t.assert.strictEqual(req.method, 'POST') 21 | t.assert.match(req.headers['content-type'], /^multipart\/form-data/) 22 | 23 | let data = '' 24 | req.setEncoding('utf8') 25 | req.on('data', (chunk) => { 26 | data += chunk 27 | }) 28 | req.on('end', () => { 29 | // Verify the multipart data contains our form fields 30 | t.assert.match(data, /Content-Disposition: form-data; name="key"/) 31 | t.assert.match(data, /value/) 32 | t.assert.match(data, /Content-Disposition: form-data; name="file"/) 33 | 34 | res.setHeader('content-type', 'application/json') 35 | res.statusCode = 200 36 | res.end(JSON.stringify({ received: 'multipart data' })) 37 | }) 38 | }) 39 | 40 | // Fastify instance with custom multipart parser (not @fastify/multipart) 41 | const fastify = Fastify() 42 | 43 | // Register custom content type parser for multipart/form-data 44 | // This allows the raw body to be passed through without parsing 45 | fastify.addContentTypeParser('multipart/form-data', function (req, body, done) { 46 | done(null, body) 47 | }) 48 | 49 | fastify.register(From) 50 | 51 | fastify.post('/', (request, reply) => { 52 | reply.from(`http://localhost:${target.address().port}`) 53 | }) 54 | 55 | t.after(() => fastify.close()) 56 | t.after(() => target.close()) 57 | 58 | await new Promise(resolve => fastify.listen({ port: 0 }, resolve)) 59 | await new Promise(resolve => target.listen({ port: 0 }, resolve)) 60 | 61 | // Create multipart form data 62 | const form = new FormData() 63 | form.append('key', 'value') 64 | form.append('file', fs.createReadStream(filetPath, { encoding: 'utf-8' })) 65 | 66 | // Send request with multipart data 67 | const result = await request(`http://localhost:${fastify.server.address().port}`, { 68 | method: 'POST', 69 | headers: form.getHeaders(), 70 | body: form 71 | }) 72 | 73 | t.assert.deepStrictEqual(await result.body.json(), { received: 'multipart data' }) 74 | }) 75 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 'transfer-encoding': 46 | case 'proxy-connection': 47 | case 'keep-alive': 48 | case 'host': 49 | break 50 | case 'te': 51 | // see illegal connection specific header handling in Node.js 52 | if (headers['te'] === 'trailers') { 53 | dest[header] = headers[header] 54 | } 55 | break 56 | default: 57 | dest[header] = headers[header] 58 | break 59 | } 60 | } 61 | return dest 62 | } 63 | 64 | // issue ref: https://github.com/fastify/fast-proxy/issues/42 65 | function buildURL (source, reqBase) { 66 | if (decodeURIComponent(source).includes('..')) { 67 | const err = new Error('source/request contain invalid characters') 68 | err.statusCode = 400 69 | throw err 70 | } 71 | 72 | if (Array.isArray(reqBase)) reqBase = reqBase[0] 73 | let baseOrigin = reqBase ? new URL(reqBase).href : undefined 74 | 75 | // To make sure we don't accidentally override the base path 76 | if (baseOrigin && source.length > 1 && source[0] === '/' && source[1] === '/') { 77 | source = '.' + source 78 | } 79 | 80 | const dest = new URL(source, reqBase) 81 | 82 | // if base is specified, source url should not override it 83 | if (baseOrigin) { 84 | if (!baseOrigin.endsWith('/') && dest.href.length > baseOrigin.length) { 85 | baseOrigin = baseOrigin + '/' 86 | } 87 | 88 | if (!dest.href.startsWith(baseOrigin)) { 89 | throw new Error('source must be a relative path string') 90 | } 91 | } 92 | 93 | return dest 94 | } 95 | 96 | module.exports = { 97 | copyHeaders, 98 | stripHttp1ConnectionHeaders, 99 | filterPseudoHeaders, 100 | buildURL 101 | } 102 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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/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.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/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/http2-canceled-streams-cleanup.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const t = require('node:test') 4 | const Fastify = require('fastify') 5 | const From = require('..') 6 | const fs = require('node:fs') 7 | const path = require('node:path') 8 | const http2 = require('node:http2') 9 | const { once } = require('events') 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 | const { HTTP2_HEADER_STATUS, HTTP2_HEADER_PATH } = http2.constants 15 | 16 | function makeRequest (client, counter) { 17 | return new Promise((resolve, reject) => { 18 | const controller = new AbortController() 19 | const signal = controller.signal 20 | const cancelRequestEarly = counter % 2 === 0 // cancel early every other request 21 | let responseCounter = 0 22 | 23 | const clientStream = client.request({ [HTTP2_HEADER_PATH]: '/' }, { signal }) 24 | 25 | clientStream.end() 26 | 27 | clientStream.on('data', chunk => { 28 | const s = chunk.toString() 29 | // Sometimes we just get NGHTTP2_ENHANCE_YOUR_CALM internal server errors 30 | if (s.startsWith('{"statusCode":500')) reject(new Error('got internal server error')) 31 | else responseCounter++ 32 | }) 33 | 34 | clientStream.on('error', err => { 35 | if (err instanceof Error && err.name === 'AbortError') { 36 | if (responseCounter === 0 && !cancelRequestEarly) { 37 | // if we didn´t cancel early we should have received at least one response from the target 38 | // if not, this indicated the stream resource leak 39 | reject(new Error('no response')) 40 | } else resolve() 41 | } else reject(err instanceof Error ? err : new Error(JSON.stringify(err))) 42 | }) 43 | 44 | clientStream.on('end', () => { resolve() }) 45 | 46 | setTimeout(() => { controller.abort() }, cancelRequestEarly ? 20 : 200) 47 | }) 48 | } 49 | 50 | const httpsOptions = { 51 | ...certs, 52 | settings: { 53 | maxConcurrentStreams: 10, // lower the default so we can reproduce the problem quicker 54 | } 55 | } 56 | 57 | t.test('http2 -> http2', async (t) => { 58 | const instance = Fastify({ 59 | http2: true, 60 | https: httpsOptions 61 | }) 62 | 63 | t.after(() => instance.close()) 64 | 65 | const target = http2.createSecureServer(httpsOptions) 66 | 67 | target.on('stream', (stream, _headers, _flags) => { 68 | let counter = 0 69 | let headerSent = false 70 | 71 | // deliberately delay sending the headers 72 | const sendData = () => { 73 | if (!headerSent) { 74 | stream.respond({ [HTTP2_HEADER_STATUS]: 200, }) 75 | headerSent = true 76 | } 77 | stream.write(counter + '\n') 78 | counter = counter + 1 79 | } 80 | 81 | const intervalId = setInterval(sendData, 50) 82 | 83 | // ignore write after end errors 84 | stream.on('error', _err => { }) 85 | 86 | stream.on('close', () => { clearInterval(intervalId) }) 87 | }) 88 | 89 | instance.get('/', (_request, reply) => { 90 | reply.from() 91 | }) 92 | 93 | t.after(() => target.close()) 94 | 95 | target.listen() 96 | await once(target, 'listening') 97 | 98 | instance.register(From, { 99 | base: `https://localhost:${target.address().port}`, 100 | http2: true, 101 | rejectUnauthorized: false 102 | }) 103 | 104 | const url = await instance.listen({ port: 0 }) 105 | 106 | const client = http2.connect(url, { 107 | rejectUnauthorized: false, 108 | }) 109 | 110 | // see https://github.com/fastify/fastify-reply-from/issues/424 111 | // without the bug fix this will fail after about 15 requests 112 | for (let i = 0; i < 30; i++) { await makeRequest(client, i) } 113 | 114 | client.close() 115 | instance.close() 116 | target.close() 117 | }) 118 | -------------------------------------------------------------------------------- /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 | 80 | test('should throw on path traversal attempts', (t) => { 81 | t.assert.throws( 82 | () => buildURL('/foo/bar/../', 'http://localhost'), 83 | new Error('source/request contain invalid characters') 84 | ) 85 | 86 | t.assert.throws( 87 | () => buildURL('/foo/bar/..', 'http://localhost'), 88 | new Error('source/request contain invalid characters') 89 | ) 90 | 91 | t.assert.throws( 92 | () => buildURL('/foo/bar/%2e%2e/', 'http://localhost'), 93 | new Error('source/request contain invalid characters') 94 | ) 95 | 96 | t.assert.throws( 97 | () => buildURL('/foo/bar/%2E%2E/', 'http://localhost'), 98 | new Error('source/request contain invalid characters') 99 | ) 100 | 101 | t.assert.throws( 102 | () => buildURL('/foo/bar/..%2f', 'http://localhost'), 103 | new Error('source/request contain invalid characters') 104 | ) 105 | 106 | t.assert.throws( 107 | () => buildURL('/foo/bar/%2e%2e%2f', 'http://localhost'), 108 | new Error('source/request contain invalid characters') 109 | ) 110 | }) 111 | -------------------------------------------------------------------------------- /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-goaway.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 http2 = require('node:http2') 10 | 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 | t.test('http2 goaway handling - reproduces issue #409', async (t) => { 17 | let requestCount = 0 18 | 19 | // Create a custom HTTP/2 server that sends GOAWAY after first request 20 | const targetServer = http2.createServer() 21 | 22 | let sessionToClose = null 23 | 24 | targetServer.on('session', (session) => { 25 | // Store the first session to send GOAWAY later 26 | if (!sessionToClose) { 27 | sessionToClose = session 28 | } 29 | }) 30 | 31 | targetServer.on('stream', (stream, headers) => { 32 | requestCount++ 33 | 34 | if (requestCount === 1) { 35 | // First request: respond normally 36 | stream.respond({ 37 | ':status': 200, 38 | 'content-type': 'application/json' 39 | }) 40 | stream.end(JSON.stringify({ request: requestCount, message: 'first request' })) 41 | 42 | // Send GOAWAY after response to close the HTTP/2 session gracefully 43 | setTimeout(() => { 44 | if (sessionToClose && !sessionToClose.destroyed) { 45 | // Send GOAWAY with NO_ERROR to close gracefully 46 | sessionToClose.goaway(0) 47 | } 48 | }, 50) 49 | } else { 50 | // Subsequent requests should work with a new session 51 | stream.respond({ 52 | ':status': 200, 53 | 'content-type': 'application/json' 54 | }) 55 | stream.end(JSON.stringify({ request: requestCount, message: 'subsequent request' })) 56 | } 57 | }) 58 | 59 | await new Promise((resolve) => { 60 | targetServer.listen(0, resolve) 61 | }) 62 | 63 | const targetPort = targetServer.address().port 64 | 65 | // Create proxy server 66 | const instance = Fastify({ 67 | http2: true, 68 | https: certs 69 | }) 70 | 71 | instance.register(From, { 72 | base: `http://localhost:${targetPort}`, 73 | http2: true, 74 | rejectUnauthorized: false 75 | }) 76 | 77 | instance.get('/', (_request, reply) => { 78 | reply.from() 79 | }) 80 | 81 | await instance.listen({ port: 0 }) 82 | 83 | const proxyPort = instance.server.address().port 84 | 85 | // First request - should succeed 86 | const firstResponse = await h2url.concat({ 87 | url: `https://localhost:${proxyPort}` 88 | }) 89 | 90 | t.assert.strictEqual(firstResponse.headers[':status'], 200) 91 | const firstBody = JSON.parse(firstResponse.body) 92 | t.assert.strictEqual(firstBody.request, 1) 93 | t.assert.strictEqual(firstBody.message, 'first request') 94 | 95 | // Wait for GOAWAY to be sent and processed 96 | await new Promise(resolve => setTimeout(resolve, 100)) 97 | 98 | // Second request - this should fail with current implementation but work with fix 99 | try { 100 | const secondResponse = await h2url.concat({ 101 | url: `https://localhost:${proxyPort}`, 102 | timeout: 1000 103 | }) 104 | 105 | // If we get here with the current code, the request succeeded 106 | // which means the issue might not be reproduced 107 | t.assert.strictEqual(secondResponse.headers[':status'], 200) 108 | const secondBody = JSON.parse(secondResponse.body) 109 | t.assert.strictEqual(secondBody.request, 2) 110 | t.assert.strictEqual(secondBody.message, 'subsequent request') 111 | console.log('Second request succeeded - issue may not be reproduced or fix is already in place') 112 | } catch (err) { 113 | // This is expected without the fix - the session is stuck in closed state 114 | console.log(`Second request failed (expected without fix): ${err.code || err.message}`) 115 | // This demonstrates the issue exists - session is stuck after GOAWAY 116 | } 117 | 118 | // Cleanup in correct order: clients first, then proxy, then server 119 | await instance.close() 120 | targetServer.close() 121 | 122 | // Force exit after a short delay to ensure test completes 123 | setTimeout(() => { 124 | process.exit(0) 125 | }, 100) 126 | }) 127 | -------------------------------------------------------------------------------- /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 | IncomingMessage, 19 | RequestOptions, 20 | } from 'node:http' 21 | import { 22 | ClientSessionOptions, 23 | ClientSessionRequestOptions, 24 | IncomingHttpHeaders as Http2IncomingHttpHeaders, 25 | SecureClientSessionOptions, 26 | } from 'node:http2' 27 | import { 28 | Agent as SecureAgent, 29 | AgentOptions as SecureAgentOptions, 30 | RequestOptions as SecureRequestOptions 31 | } from 'node:https' 32 | import { Pool, ProxyAgent, Dispatcher } from 'undici' 33 | 34 | declare module 'fastify' { 35 | interface FastifyReply { 36 | from( 37 | source?: string, 38 | opts?: fastifyReplyFrom.FastifyReplyFromHooks 39 | ): this; 40 | } 41 | } 42 | 43 | type FastifyReplyFrom = FastifyPluginCallback 44 | declare namespace fastifyReplyFrom { 45 | type QueryStringFunction = ( 46 | search: string | undefined, 47 | reqUrl: string, 48 | request: FastifyRequest 49 | ) => string 50 | 51 | export type RetryDetails = { 52 | err: Error; 53 | req: FastifyRequest; 54 | res: FastifyReply; 55 | attempt: number; 56 | retriesCount: number; 57 | getDefaultDelay: ( 58 | req: FastifyRequest, 59 | res: FastifyReply, 60 | err: Error, 61 | retries: number, 62 | ) => number | null; 63 | } 64 | 65 | export type RawServerResponse = RawReplyDefaultExpression & { 66 | stream: IncomingMessage 67 | } 68 | 69 | export interface FastifyReplyFromHooks { 70 | queryString?: { [key: string]: unknown } | QueryStringFunction; 71 | contentType?: string; 72 | retryDelay?: (details: RetryDetails) => number | null; 73 | retriesCount?: number; 74 | onResponse?: ( 75 | request: FastifyRequest, 76 | reply: FastifyReply, 77 | res: RawServerResponse 78 | ) => void; 79 | onError?: ( 80 | reply: FastifyReply, 81 | error: { error: Error } 82 | ) => void; 83 | body?: unknown; 84 | rewriteHeaders?: ( 85 | headers: Http2IncomingHttpHeaders | IncomingHttpHeaders, 86 | request?: FastifyRequest 87 | ) => Http2IncomingHttpHeaders | IncomingHttpHeaders; 88 | rewriteRequestHeaders?: ( 89 | request: FastifyRequest, 90 | headers: Http2IncomingHttpHeaders | IncomingHttpHeaders 91 | ) => Http2IncomingHttpHeaders | IncomingHttpHeaders; 92 | getUpstream?: ( 93 | request: FastifyRequest, 94 | base: string 95 | ) => string; 96 | method?: HTTPMethods; 97 | timeout?: number; 98 | } 99 | 100 | interface Http2Options { 101 | sessionTimeout?: number; 102 | requestTimeout?: number; 103 | sessionOptions?: ClientSessionOptions | SecureClientSessionOptions; 104 | requestOptions?: ClientSessionRequestOptions; 105 | } 106 | 107 | interface HttpOptions { 108 | agentOptions?: AgentOptions | SecureAgentOptions; 109 | requestOptions?: RequestOptions | SecureRequestOptions; 110 | agents?: { 'http:': Agent, 'https:': SecureAgent } 111 | } 112 | 113 | export interface FastifyReplyFromOptions { 114 | base?: string | string[]; 115 | cacheURLs?: number; 116 | disableCache?: boolean; 117 | http?: HttpOptions; 118 | http2?: Http2Options | boolean; 119 | undici?: Pool.Options & { proxy?: string | URL | ProxyAgent.Options } | { request: Dispatcher['request'] }; 120 | balancedPoolOptions?: Pool.Options & Record; 121 | contentTypesToEncode?: string[]; 122 | retryMethods?: (HTTPMethods | 'TRACE')[]; 123 | maxRetriesOn503?: number; 124 | disableRequestLogging?: boolean; 125 | globalAgent?: boolean; 126 | destroyAgent?: boolean; 127 | } 128 | 129 | export const fastifyReplyFrom: FastifyReplyFrom 130 | export { fastifyReplyFrom as default } 131 | } 132 | 133 | declare function fastifyReplyFrom (...params: Parameters): ReturnType 134 | export = fastifyReplyFrom 135 | -------------------------------------------------------------------------------- /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/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 | --------------------------------------------------------------------------------