Skip to content

Commit 0afe34c

Browse files
authored
Feat/ balancedPool add (#421)
* feat: support array of base URLs for undici balancedPool in request options * feat: enhance base URL option to support array for load balancing with undici.BalancedPool * fix: correct typos in README.md
1 parent d41783c commit 0afe34c

File tree

5 files changed

+82
-3
lines changed

5 files changed

+82
-3
lines changed

README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,15 @@ target.listen({ port: 3001 }, (err) => {
6969
Set the base URL for all the forwarded requests. Will be required if `http2` is set to `true`
7070
Note that _every path will be discarded_.
7171

72+
Set the base URL for all the forwarded requests.
73+
*String or String[]*:
74+
75+
* **Single string** → a normal `undici.Pool` / `http.request` client is used.
76+
* **Array with ≥ 2 elements****[`undici.BalancedPool`](https://undici.nodejs.org/#/docs/api/BalancedPool)** is automatically selected and requests are load-balanced round-robin across the given origins.
77+
78+
When you provide an array, only the *origin* (`protocol://host:port`) part of each URL is considered; any path component is ignored.
79+
80+
7281
Custom URL protocols `unix+http:` and `unix+https:` can be used to forward requests to a unix
7382
socket server by using `querystring.escape(socketPath)` as the hostname. This is not supported
7483
for http2 nor undici. To illustrate:
@@ -125,6 +134,17 @@ proxy.register(require('@fastify/reply-from'), {
125134
})
126135
```
127136

137+
You can also use with BalancedPool:
138+
```js
139+
proxy.register(require('@fastify/reply-from'), {
140+
base: [
141+
'http://api-1.internal:8080',
142+
'http://api-2.internal:8080',
143+
'http://api-3.internal:8080'
144+
]
145+
})
146+
```
147+
128148
#### `http`
129149

130150
Set the `http` option to an Object to use

lib/request.js

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ function isUndiciInstance (obj) {
3737

3838
function buildRequest (opts) {
3939
const isHttp2 = !!opts.http2
40+
if (Array.isArray(opts.base) && opts.base.length === 1) {
41+
opts.base = opts.base[0]
42+
}
4043
const hasUndiciOptions = shouldUseUndici(opts)
4144
const requests = {
4245
'http:': http,
@@ -46,7 +49,8 @@ function buildRequest (opts) {
4649
}
4750
const http2Opts = getHttp2Opts(opts)
4851
const httpOpts = getHttpOpts(opts)
49-
const baseUrl = opts.base && new URL(opts.base).origin
52+
const baseUrl = Array.isArray(opts.base) ? null : (opts.base && new URL(opts.base).origin)
53+
const isBalanced = Array.isArray(opts.base) && opts.base.length > 1
5054
const undiciOpts = opts.undici || {}
5155
const globalAgent = opts.globalAgent
5256
const destroyAgent = opts.destroyAgent
@@ -75,7 +79,10 @@ function buildRequest (opts) {
7579
if (isHttp2) {
7680
return { request: handleHttp2Req, close, retryOnError: 'ECONNRESET' }
7781
} else if (hasUndiciOptions) {
78-
if (opts.base?.startsWith('unix+')) {
82+
if (isBalanced) {
83+
const origins = opts.base.map(u => new URL(u).origin)
84+
undiciInstance = new undici.BalancedPool(origins, getUndiciOptions(opts.undici))
85+
} else if (opts.base?.startsWith('unix+')) {
7986
const undiciOpts = getUndiciOptions(opts.undici)
8087
undiciOpts.socketPath = decodeURIComponent(new URL(opts.base).host)
8188
const protocol = opts.base.startsWith('unix+https') ? 'https' : 'http'
@@ -155,6 +162,8 @@ function buildRequest (opts) {
155162

156163
if (undiciInstance) {
157164
pool = undiciInstance
165+
} else if (pool instanceof undici.BalancedPool) {
166+
delete req.origin
158167
} else if (!baseUrl && opts.url.protocol.startsWith('unix')) {
159168
done(new Error('unix socket not supported with undici yet'))
160169
return

lib/utils.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ function stripHttp1ConnectionHeaders (headers) {
5858

5959
// issue ref: https://github.com/fastify/fast-proxy/issues/42
6060
function buildURL (source, reqBase) {
61+
if (Array.isArray(reqBase)) reqBase = reqBase[0]
6162
let baseOrigin = reqBase ? new URL(reqBase).href : undefined
6263

6364
// To make sure we don't accidentally override the base path

test/balanced-pool.test.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
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+
})

types/index.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,12 +100,13 @@ declare namespace fastifyReplyFrom {
100100
}
101101

102102
export interface FastifyReplyFromOptions {
103-
base?: string;
103+
base?: string | string[];
104104
cacheURLs?: number;
105105
disableCache?: boolean;
106106
http?: HttpOptions;
107107
http2?: Http2Options | boolean;
108108
undici?: Pool.Options & { proxy?: string | URL | ProxyAgent.Options } | { request: Dispatcher['request'] };
109+
balancedPoolOptions?: Pool.Options & Record<string, unknown>;
109110
contentTypesToEncode?: string[];
110111
retryMethods?: (HTTPMethods | 'TRACE')[];
111112
maxRetriesOn503?: number;

0 commit comments

Comments
 (0)