├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── package.json ├── LICENSE ├── README.md ├── .gitignore ├── test ├── listening.test.js ├── interceptor.test.js └── basic.test.js └── index.js /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | ignore: 9 | - dependency-name: standard 10 | versions: 11 | - 16.0.3 12 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | - 'docs/**' 7 | - '*.md' 8 | pull_request: 9 | paths-ignore: 10 | - 'docs/**' 11 | - '*.md' 12 | 13 | jobs: 14 | test: 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [20.x, 22.x] 20 | steps: 21 | - uses: actions/checkout@v3 22 | 23 | - name: Use Node.js 24 | uses: actions/setup-node@v2 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | 28 | - name: Install 29 | run: | 30 | npm install 31 | 32 | - name: Run tests 33 | run: | 34 | npm run test 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fastify-undici-dispatcher", 3 | "version": "0.7.0", 4 | "description": "An undici dispatcher to in-process Fastify servers", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "standard | snazzy && c8 --100 node --test test/*.test.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/mcollina/fastify-undici-dispatcher.git" 12 | }, 13 | "keywords": [], 14 | "author": "Matteo Collina ", 15 | "license": "MIT", 16 | "bugs": { 17 | "url": "https://github.com/mcollina/fastify-undici-dispatcher/issues" 18 | }, 19 | "homepage": "https://github.com/mcollina/fastify-undici-dispatcher#readme", 20 | "devDependencies": { 21 | "@fastify/express": "^4.0.0", 22 | "c8": "^10.0.0", 23 | "express": "^4.18.2", 24 | "fastify": "^5.0.0", 25 | "snazzy": "^9.0.0", 26 | "standard": "^17.0.0" 27 | }, 28 | "dependencies": { 29 | "undici": "^6.9.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Matteo Collina 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fastify-undici-dispatcher 2 | 3 | An undici dispatcher to in-process Fastify servers 4 | 5 | ## Install 6 | 7 | ``` 8 | npm i fastify fastify-undici-dispatcher undici 9 | ``` 10 | 11 | ## Usage as an Interceptor 12 | 13 | ```js 14 | const { request, Agent } = require('undici') 15 | const { createFastifyInterceptor } = require('fastify-undici-dispatcher') 16 | const Fastify = require('fastify') 17 | const server = Fastify() 18 | server.get('/', async (req, reply) => { 19 | return 'hello world' 20 | }) 21 | 22 | const interceptor = createFastifyInterceptor({ 23 | domain: '.local' // optional 24 | }) 25 | 26 | const dispatcher = new Agent().compose(interceptor) 27 | dispatcher.route('myserver', server) 28 | 29 | request('http://myserver.local', { 30 | dispatcher 31 | }).then((res) => { 32 | return res.body.text() 33 | }).then(console.log) 34 | ``` 35 | 36 | ## Usage as a Dispatcher 37 | 38 | ```js 39 | const { request, Agent } = require('undici') 40 | const FastifyUndiciDispatcher = require('fastify-undici-dispatcher') 41 | const Fastify = require('fastify') 42 | const server = Fastify() 43 | server.get('/', async (req, reply) => { 44 | return 'hello world' 45 | }) 46 | 47 | const dispatcher = new FastifyUndiciDispatcher({ 48 | dispatcher: new Agent(), // optional 49 | domain: '.local' // optional 50 | }) 51 | dispatcher.route('myserver', server) 52 | 53 | request('http://myserver.local', { 54 | dispatcher 55 | }).then((res) => { 56 | return res.body.text() 57 | }).then(console.log) 58 | ``` 59 | 60 | ## License 61 | 62 | MIT 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | -------------------------------------------------------------------------------- /test/listening.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('node:test') 4 | const assert = require('assert') 5 | const { request, Agent } = require('undici') 6 | const FastifyUndiciDispatcher = require('..') 7 | const Fastify = require('fastify') 8 | const FastifyExpress = require('@fastify/express') 9 | const { Router } = require('express') 10 | 11 | test('listening server', async (t) => { 12 | const server = Fastify() 13 | server.get('/', async (req, reply) => { 14 | return 'root' 15 | }) 16 | server.get('/foo', async (req, reply) => { 17 | assert.notStrictEqual(req.headers['user-agent'], 'lightMyRequest') 18 | return 'foo' 19 | }) 20 | await server.listen({ port: 0 }) 21 | t.after(() => server.close()) 22 | 23 | const dispatcher = new FastifyUndiciDispatcher({ 24 | dispatcher: new Agent() 25 | }) 26 | dispatcher.route('myserver.local', server) 27 | 28 | { 29 | const res = await request('http://myserver.local/', { dispatcher }) 30 | assert.strictEqual(await res.body.text(), 'root') 31 | } 32 | 33 | { 34 | const res = await request('http://myserver.local/foo', { dispatcher }) 35 | assert.strictEqual(await res.body.text(), 'foo') 36 | } 37 | }) 38 | 39 | test('supports @fastify/express', async (t) => { 40 | const server = Fastify() 41 | 42 | const router = Router() 43 | 44 | router.get('/', (req, res) => { 45 | res.send('root') 46 | }) 47 | router.get('/foo', (req, res) => { 48 | res.send('foo') 49 | }) 50 | 51 | await server.register(FastifyExpress) 52 | server.use(router) 53 | 54 | await server.listen({ port: 0 }) 55 | t.after(() => server.close()) 56 | 57 | const dispatcher = new FastifyUndiciDispatcher({ 58 | dispatcher: new Agent() 59 | }) 60 | dispatcher.route('myserver.local', server) 61 | 62 | { 63 | const res = await request('http://myserver.local/', { dispatcher }) 64 | assert.strictEqual(await res.body.text(), 'root') 65 | } 66 | 67 | { 68 | const res = await request('http://myserver.local/foo', { dispatcher }) 69 | assert.strictEqual(await res.body.text(), 'foo') 70 | } 71 | }) 72 | 73 | test('supports @fastify/express (ipv4)', async (t) => { 74 | const server = Fastify() 75 | 76 | const router = Router() 77 | 78 | router.get('/', (req, res) => { 79 | res.send('root') 80 | }) 81 | router.get('/foo', (req, res) => { 82 | res.send('foo') 83 | }) 84 | 85 | await server.register(FastifyExpress) 86 | server.use(router) 87 | 88 | await server.listen({ port: 0, host: '127.0.0.1' }) 89 | t.after(() => server.close()) 90 | 91 | const dispatcher = new FastifyUndiciDispatcher({ 92 | dispatcher: new Agent() 93 | }) 94 | dispatcher.route('myserver.local', server) 95 | 96 | { 97 | const res = await request('http://myserver.local/', { dispatcher }) 98 | assert.strictEqual(await res.body.text(), 'root') 99 | } 100 | 101 | { 102 | const res = await request('http://myserver.local/foo', { dispatcher }) 103 | assert.strictEqual(await res.body.text(), 'foo') 104 | } 105 | }) 106 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { Dispatcher } = require('undici') 4 | 5 | function createFastifyInterceptor (opts) { 6 | const routes = new Map() 7 | const domain = opts?.domain 8 | const res = (dispatch) => { 9 | return (opts, handler) => { 10 | let url = opts.origin 11 | if (!(url instanceof URL)) { 12 | url = new URL(opts.path, url) 13 | } 14 | 15 | const wrap = routes.get(url.hostname) 16 | if (!wrap) { 17 | if (dispatch && (domain === undefined || !url.hostname.endsWith(domain))) { 18 | return dispatch(opts, handler) 19 | } else { 20 | throw new Error('No server found for ' + url.hostname) 21 | } 22 | } 23 | 24 | const { server, address } = wrap 25 | 26 | if (opts.headers) { 27 | delete opts.headers.connection 28 | delete opts.headers['transfer-encoding'] 29 | } 30 | 31 | if (address) { 32 | if (address.family === 'IPv6') { 33 | opts.origin = `http://[${address.address}]:${address.port}` 34 | } else { 35 | opts.origin = `http://${address.address}:${address.port}` 36 | } 37 | 38 | return dispatch(opts, handler) 39 | } 40 | 41 | server.inject({ 42 | method: opts.method, 43 | url: url.pathname + url.search, 44 | headers: opts.headers, 45 | query: opts.query, 46 | body: opts.body 47 | }).then(res => { 48 | const headers = [] 49 | for (const [key, value] of Object.entries(res.headers)) { 50 | if (Array.isArray(value)) { 51 | for (const v of value) { 52 | headers.push(key) 53 | headers.push(v) 54 | } 55 | } else { 56 | headers.push(key) 57 | headers.push(value) 58 | } 59 | } 60 | handler.onHeaders(res.statusCode, headers, () => {}, res.statusMessage) 61 | handler.onData(res.rawPayload) 62 | handler.onComplete([]) 63 | /* c8 ignore next 3 */ 64 | }).catch(err => { 65 | handler.onError(err) 66 | }) 67 | return true 68 | } 69 | } 70 | 71 | res.route = (url, server) => { 72 | if (domain && !url.endsWith(domain)) { 73 | url += domain 74 | } 75 | const address = server.server.address() 76 | routes.set(url, { server, address }) 77 | } 78 | 79 | return res 80 | } 81 | 82 | class FastifyUndiciDispatcher extends Dispatcher { 83 | constructor (opts) { 84 | super() 85 | this.dispatcher = opts?.dispatcher 86 | const interceptor = createFastifyInterceptor(opts) 87 | this.route = interceptor.route 88 | if (this.dispatcher) { 89 | this.dispatch = interceptor(this.dispatcher.dispatch.bind(this.dispatcher)) 90 | } else { 91 | this.dispatch = interceptor() 92 | } 93 | } 94 | 95 | close () { 96 | if (this.dispatcher) { 97 | return this.dispatcher.close() 98 | } 99 | } 100 | 101 | destroy () { 102 | if (this.dispatcher) { 103 | return this.dispatcher.destroy() 104 | } 105 | } 106 | } 107 | 108 | module.exports = FastifyUndiciDispatcher 109 | module.exports.createFastifyInterceptor = createFastifyInterceptor 110 | -------------------------------------------------------------------------------- /test/interceptor.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('node:test') 4 | const assert = require('assert') 5 | const { request, Agent } = require('undici') 6 | const { createFastifyInterceptor } = require('..') 7 | const Fastify = require('fastify') 8 | 9 | test('basic usage', async (t) => { 10 | const server = Fastify() 11 | server.get('/', async (req, reply) => { 12 | return 'root' 13 | }) 14 | server.get('/foo', async (req, reply) => { 15 | return 'foo' 16 | }) 17 | await server.ready() 18 | 19 | const interceptor = createFastifyInterceptor() 20 | interceptor.route('myserver.local', server) 21 | 22 | const dispatcher = new Agent() 23 | .compose(interceptor) 24 | 25 | { 26 | const res = await request('http://myserver.local/', { dispatcher }) 27 | assert.strictEqual(await res.body.text(), 'root') 28 | } 29 | 30 | { 31 | const res = await request('http://myserver.local/foo', { dispatcher }) 32 | assert.strictEqual(await res.body.text(), 'foo') 33 | } 34 | }) 35 | 36 | test('pass-through', async (t) => { 37 | const server = Fastify() 38 | server.get('/', async (req, reply) => { 39 | return 'hello world 2' 40 | }) 41 | await server.listen({ port: 0 }) 42 | 43 | const dispatcher = new Agent().compose(createFastifyInterceptor()) 44 | t.after(() => dispatcher.close()) 45 | t.after(() => server.close()) 46 | 47 | const res = await request(`http://127.0.0.1:${server.addresses()[0].port}/`, { 48 | dispatcher 49 | }) 50 | 51 | const text = await res.body.text() 52 | assert.strictEqual(text, 'hello world 2') 53 | }) 54 | 55 | test('pass-through query string', async (t) => { 56 | const server = Fastify() 57 | server.get('/query', async (req, reply) => { 58 | return req.query.test 59 | }) 60 | await server.listen({ port: 0 }) 61 | 62 | const dispatcher = new Agent().compose(createFastifyInterceptor()) 63 | 64 | t.after(() => dispatcher.close()) 65 | t.after(() => server.close()) 66 | 67 | const res = await request(`http://127.0.0.1:${server.addresses()[0].port}/query?test=test`, { 68 | dispatcher 69 | }) 70 | 71 | const text = await res.body.text() 72 | assert.strictEqual(text, 'test', 'query string not passed through') 73 | }) 74 | 75 | test('no server found', async (t) => { 76 | const dispatcher = new Agent().compose(createFastifyInterceptor({ 77 | domain: '.local' 78 | })) 79 | 80 | await assert.rejects(request('http://myserver.local/', { 81 | dispatcher 82 | }), new Error('No server found for myserver.local')) 83 | }) 84 | 85 | test('array headers', async (t) => { 86 | const server = Fastify() 87 | server.get('/', async (req, reply) => { 88 | reply.header('x-foo', ['bar', 'baz']) 89 | return 'hello world' 90 | }) 91 | await server.ready() 92 | 93 | const interceptor = createFastifyInterceptor() 94 | interceptor.route('myserver.local', server) 95 | 96 | const dispatcher = new Agent().compose(interceptor) 97 | 98 | const res = await request('http://myserver.local/', { 99 | dispatcher 100 | }) 101 | 102 | assert.deepStrictEqual(res.headers['x-foo'], ['bar', 'baz']) 103 | assert.strictEqual(await res.body.text(), 'hello world') 104 | }) 105 | 106 | test('removes unwanted headers', async (t) => { 107 | const server = Fastify() 108 | server.get('/', async (req, reply) => { 109 | return 'root' 110 | }) 111 | await server.ready() 112 | 113 | const interceptor = createFastifyInterceptor() 114 | interceptor.route('myserver.local', server) 115 | 116 | const dispatcher = new Agent().compose(interceptor) 117 | 118 | const res = await dispatcher.request({ 119 | origin: 'http://myserver.local/', 120 | path: '/', 121 | headers: { 122 | connection: undefined, 123 | 'transfer-encoding': undefined 124 | } 125 | }) 126 | assert.strictEqual(await res.body.text(), 'root') 127 | }) 128 | 129 | test('automatic domain', async (t) => { 130 | const server = Fastify() 131 | server.get('/', async (req, reply) => { 132 | return 'root' 133 | }) 134 | server.get('/foo', async (req, reply) => { 135 | return 'foo' 136 | }) 137 | await server.ready() 138 | 139 | const interceptor = createFastifyInterceptor({ domain: '.local' }) 140 | interceptor.route('myserver', server) 141 | const dispatcher = new Agent().compose(interceptor) 142 | 143 | { 144 | const res = await request('http://myserver.local/', { dispatcher }) 145 | assert.strictEqual(await res.body.text(), 'root') 146 | } 147 | 148 | { 149 | const res = await request('http://myserver.local/foo', { dispatcher }) 150 | assert.strictEqual(await res.body.text(), 'foo') 151 | } 152 | }) 153 | 154 | test('automatic domain / 2', async (t) => { 155 | const server = Fastify() 156 | server.get('/', async (req, reply) => { 157 | return 'root' 158 | }) 159 | server.get('/foo', async (req, reply) => { 160 | return 'foo' 161 | }) 162 | await server.ready() 163 | 164 | const interceptor = createFastifyInterceptor({ domain: '.local' }) 165 | interceptor.route('myserver', server) 166 | const dispatcher = new Agent().compose(interceptor) 167 | 168 | { 169 | const res = await request('http://myserver.local/', { dispatcher }) 170 | assert.strictEqual(await res.body.text(), 'root') 171 | } 172 | 173 | { 174 | const res = await request('http://myserver.local/foo', { dispatcher }) 175 | assert.strictEqual(await res.body.text(), 'foo') 176 | } 177 | }) 178 | 179 | test('binary response', async (t) => { 180 | const server = Fastify() 181 | 182 | const randomBuffer = Buffer.alloc(4) 183 | randomBuffer.writeUInt32BE(2424213242) 184 | 185 | server.get('/', async (req, reply) => { 186 | reply.header('content-type', 'application/octet-stream') 187 | reply.send(randomBuffer) 188 | }) 189 | await server.ready() 190 | 191 | const interceptor = createFastifyInterceptor() 192 | interceptor.route('myserver.local', server) 193 | const dispatcher = new Agent().compose(interceptor) 194 | 195 | const res = await dispatcher.request({ 196 | origin: 'http://myserver.local/', 197 | path: '/' 198 | }) 199 | 200 | const data = await res.body.arrayBuffer() 201 | assert.deepEqual(Buffer.from(data), randomBuffer) 202 | }) 203 | 204 | test('pass query params', async (t) => { 205 | const server = Fastify() 206 | server.get('/query', async (req, reply) => { 207 | return req.query.test 208 | }) 209 | await server.ready() 210 | 211 | const interceptor = createFastifyInterceptor() 212 | interceptor.route('myserver.local', server) 213 | const dispatcher = new Agent().compose(interceptor) 214 | 215 | const res = await dispatcher.request({ 216 | origin: 'http://myserver.local/', 217 | path: '/query', 218 | query: { test: 'foo' } 219 | }) 220 | assert.strictEqual(await res.body.text(), 'foo') 221 | }) 222 | 223 | test('pass query string', async (t) => { 224 | const server = Fastify() 225 | server.get('/query', async (req, reply) => { 226 | return req.query.test 227 | }) 228 | await server.ready() 229 | 230 | const interceptor = createFastifyInterceptor() 231 | interceptor.route('myserver.local', server) 232 | const dispatcher = new Agent().compose(interceptor) 233 | 234 | const res = await dispatcher.request({ 235 | origin: 'http://myserver.local/', 236 | path: '/query?test=foo' 237 | }) 238 | assert.strictEqual(await res.body.text(), 'foo') 239 | }) 240 | -------------------------------------------------------------------------------- /test/basic.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('node:test') 4 | const assert = require('assert') 5 | const { request, Agent } = require('undici') 6 | const FastifyUndiciDispatcher = require('..') 7 | const Fastify = require('fastify') 8 | 9 | test('basic usage', async (t) => { 10 | const server = Fastify() 11 | server.get('/', async (req, reply) => { 12 | return 'root' 13 | }) 14 | server.get('/foo', async (req, reply) => { 15 | return 'foo' 16 | }) 17 | await server.ready() 18 | 19 | const dispatcher = new FastifyUndiciDispatcher() 20 | dispatcher.route('myserver.local', server) 21 | 22 | { 23 | const res = await request('http://myserver.local/', { dispatcher }) 24 | assert.strictEqual(await res.body.text(), 'root') 25 | } 26 | 27 | { 28 | const res = await request('http://myserver.local/foo', { dispatcher }) 29 | assert.strictEqual(await res.body.text(), 'foo') 30 | } 31 | }) 32 | 33 | test('pass-through', async (t) => { 34 | const server = Fastify() 35 | server.get('/', async (req, reply) => { 36 | return 'hello world 2' 37 | }) 38 | await server.listen({ port: 0 }) 39 | 40 | const dispatcher = new FastifyUndiciDispatcher({ 41 | dispatcher: new Agent() 42 | }) 43 | t.after(() => dispatcher.close()) 44 | t.after(() => server.close()) 45 | 46 | const res = await request(`http://127.0.0.1:${server.addresses()[0].port}/`, { 47 | dispatcher 48 | }) 49 | 50 | const text = await res.body.text() 51 | assert.strictEqual(text, 'hello world 2') 52 | }) 53 | 54 | test('pass-through query string', async (t) => { 55 | const server = Fastify() 56 | server.get('/query', async (req, reply) => { 57 | return req.query.test 58 | }) 59 | await server.listen({ port: 0 }) 60 | 61 | const dispatcher = new FastifyUndiciDispatcher({ 62 | dispatcher: new Agent() 63 | }) 64 | t.after(() => dispatcher.close()) 65 | t.after(() => server.close()) 66 | 67 | const res = await request(`http://127.0.0.1:${server.addresses()[0].port}/query?test=test`, { 68 | dispatcher 69 | }) 70 | 71 | const text = await res.body.text() 72 | assert.strictEqual(text, 'test', 'query string not passed through') 73 | }) 74 | 75 | test('no server found', async (t) => { 76 | const dispatcher = new FastifyUndiciDispatcher() 77 | 78 | await assert.rejects(request('http://myserver.local/', { 79 | dispatcher 80 | }), new Error('No server found for myserver.local')) 81 | }) 82 | 83 | test('array headers', async (t) => { 84 | const server = Fastify() 85 | server.get('/', async (req, reply) => { 86 | reply.header('x-foo', ['bar', 'baz']) 87 | return 'hello world' 88 | }) 89 | await server.ready() 90 | 91 | const dispatcher = new FastifyUndiciDispatcher() 92 | dispatcher.route('myserver.local', server) 93 | 94 | const res = await request('http://myserver.local/', { 95 | dispatcher 96 | }) 97 | 98 | assert.deepStrictEqual(res.headers['x-foo'], ['bar', 'baz']) 99 | assert.strictEqual(await res.body.text(), 'hello world') 100 | }) 101 | 102 | test('should destroy the dispatcher', async (t) => { 103 | const dispatcher = new FastifyUndiciDispatcher() 104 | dispatcher.destroy() 105 | }) 106 | 107 | test('should destroy the dispatcher', async (t) => { 108 | let destroyed = false 109 | const dispatcher = new FastifyUndiciDispatcher({ 110 | dispatcher: { 111 | dispatch () { 112 | }, 113 | destroy () { 114 | destroyed = true 115 | } 116 | } 117 | }) 118 | dispatcher.destroy() 119 | assert.strictEqual(destroyed, true) 120 | }) 121 | 122 | test('dispatcher.request()', async (t) => { 123 | const server = Fastify() 124 | server.get('/', async (req, reply) => { 125 | return 'root' 126 | }) 127 | server.get('/foo', async (req, reply) => { 128 | return 'foo' 129 | }) 130 | await server.ready() 131 | 132 | const dispatcher = new FastifyUndiciDispatcher() 133 | dispatcher.route('myserver.local', server) 134 | 135 | { 136 | const res = await dispatcher.request({ origin: 'http://myserver.local/', path: '/' }) 137 | assert.strictEqual(await res.body.text(), 'root') 138 | } 139 | 140 | { 141 | const res = await dispatcher.request({ origin: 'http://myserver.local/', path: '/foo' }) 142 | assert.strictEqual(await res.body.text(), 'foo') 143 | } 144 | }) 145 | 146 | test('removes unwanted headers', async (t) => { 147 | const server = Fastify() 148 | server.get('/', async (req, reply) => { 149 | return 'root' 150 | }) 151 | await server.ready() 152 | 153 | const dispatcher = new FastifyUndiciDispatcher() 154 | dispatcher.route('myserver.local', server) 155 | 156 | const res = await dispatcher.request({ 157 | origin: 'http://myserver.local/', 158 | path: '/', 159 | headers: { 160 | connection: undefined, 161 | 'transfer-encoding': undefined 162 | } 163 | }) 164 | assert.strictEqual(await res.body.text(), 'root') 165 | }) 166 | 167 | test('quick fail', { timeout: 500 }, async (t) => { 168 | const dispatcher = new FastifyUndiciDispatcher({ 169 | dispatcher: new Agent(), 170 | domain: '.local' 171 | }) 172 | 173 | await assert.rejects(request('http://myserver.local/', { dispatcher }), new Error('No server found for myserver.local')) 174 | }) 175 | 176 | test('automatic domain', async (t) => { 177 | const server = Fastify() 178 | server.get('/', async (req, reply) => { 179 | return 'root' 180 | }) 181 | server.get('/foo', async (req, reply) => { 182 | return 'foo' 183 | }) 184 | await server.ready() 185 | 186 | const dispatcher = new FastifyUndiciDispatcher({ 187 | domain: '.local' 188 | }) 189 | dispatcher.route('myserver', server) 190 | 191 | { 192 | const res = await request('http://myserver.local/', { dispatcher }) 193 | assert.strictEqual(await res.body.text(), 'root') 194 | } 195 | 196 | { 197 | const res = await request('http://myserver.local/foo', { dispatcher }) 198 | assert.strictEqual(await res.body.text(), 'foo') 199 | } 200 | }) 201 | 202 | test('automatic domain / 2', async (t) => { 203 | const server = Fastify() 204 | server.get('/', async (req, reply) => { 205 | return 'root' 206 | }) 207 | server.get('/foo', async (req, reply) => { 208 | return 'foo' 209 | }) 210 | await server.ready() 211 | 212 | const dispatcher = new FastifyUndiciDispatcher({ 213 | domain: '.local' 214 | }) 215 | dispatcher.route('myserver.local', server) 216 | 217 | { 218 | const res = await request('http://myserver.local/', { dispatcher }) 219 | assert.strictEqual(await res.body.text(), 'root') 220 | } 221 | 222 | { 223 | const res = await request('http://myserver.local/foo', { dispatcher }) 224 | assert.strictEqual(await res.body.text(), 'foo') 225 | } 226 | }) 227 | 228 | test('binary response', async (t) => { 229 | const server = Fastify() 230 | 231 | const randomBuffer = Buffer.alloc(4) 232 | randomBuffer.writeUInt32BE(2424213242) 233 | 234 | server.get('/', async (req, reply) => { 235 | reply.header('content-type', 'application/octet-stream') 236 | reply.send(randomBuffer) 237 | }) 238 | await server.ready() 239 | 240 | const dispatcher = new FastifyUndiciDispatcher() 241 | dispatcher.route('myserver.local', server) 242 | 243 | const res = await dispatcher.request({ 244 | origin: 'http://myserver.local/', 245 | path: '/' 246 | }) 247 | 248 | const data = await res.body.arrayBuffer() 249 | assert.deepEqual(Buffer.from(data), randomBuffer) 250 | }) 251 | 252 | test('pass query params', async (t) => { 253 | const server = Fastify() 254 | server.get('/query', async (req, reply) => { 255 | return req.query.test 256 | }) 257 | await server.ready() 258 | 259 | const dispatcher = new FastifyUndiciDispatcher() 260 | dispatcher.route('myserver.local', server) 261 | 262 | const res = await dispatcher.request({ 263 | origin: 'http://myserver.local/', 264 | path: '/query', 265 | query: { test: 'foo' } 266 | }) 267 | assert.strictEqual(await res.body.text(), 'foo') 268 | }) 269 | 270 | test('pass query string', async (t) => { 271 | const server = Fastify() 272 | server.get('/query', async (req, reply) => { 273 | return req.query.test 274 | }) 275 | await server.ready() 276 | 277 | const dispatcher = new FastifyUndiciDispatcher() 278 | dispatcher.route('myserver.local', server) 279 | 280 | const res = await dispatcher.request({ 281 | origin: 'http://myserver.local/', 282 | path: '/query?test=foo' 283 | }) 284 | assert.strictEqual(await res.body.text(), 'foo') 285 | }) 286 | --------------------------------------------------------------------------------