@fastify/reply-from 12.5.0 → 12.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE CHANGED
@@ -1,9 +1,7 @@
1
1
  MIT License
2
2
 
3
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.
4
+ Copyright (c) 2017-present The Fastify team <https://github.com/fastify/fastify#team>
7
5
 
8
6
  Permission is hereby granted, free of charge, to any person obtaining a copy
9
7
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -66,10 +66,10 @@ target.listen({ port: 3001 }, (err) => {
66
66
  Set the base URL for all the forwarded requests. Will be required if `http2` is set to `true`
67
67
  Note that _every path will be discarded_.
68
68
 
69
- Set the base URL for all the forwarded requests.
70
- *String or String[]*:
69
+ Set the base URL for all the forwarded requests.
70
+ *String or String[]*:
71
71
 
72
- * **Single string** → a normal `undici.Pool` / `http.request` client is used.
72
+ * **Single string** → a normal `undici.Pool` / `http.request` client is used.
73
73
  * **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.
74
74
 
75
75
  When you provide an array, only the *origin* (`protocol://host:port`) part of each URL is considered; any path component is ignored.
package/index.js CHANGED
@@ -250,11 +250,13 @@ const fastifyReplyFrom = fp(function from (fastify, opts, next) {
250
250
 
251
251
  function getQueryString (search, reqUrl, opts, request) {
252
252
  if (typeof opts.queryString === 'function') {
253
- return '?' + opts.queryString(search, reqUrl, request)
253
+ const qs = opts.queryString(search, reqUrl, request)
254
+ return qs ? '?' + qs : ''
254
255
  }
255
256
 
256
257
  if (opts.queryString) {
257
- return '?' + querystring.stringify(opts.queryString)
258
+ const qs = querystring.stringify(opts.queryString)
259
+ return qs ? '?' + qs : ''
258
260
  }
259
261
 
260
262
  if (search.length > 0) {
package/lib/request.js CHANGED
@@ -5,7 +5,7 @@ const querystring = require('node:querystring')
5
5
  const eos = require('end-of-stream')
6
6
  const { pipeline } = require('node:stream')
7
7
  const undici = require('undici')
8
- const { stripHttp1ConnectionHeaders } = require('./utils')
8
+ const { stripHttp1ConnectionHeaders, getConnectionHeaders } = require('./utils')
9
9
  const http2 = require('node:http2')
10
10
 
11
11
  const {
@@ -53,7 +53,7 @@ function buildRequest (opts) {
53
53
  const isBalanced = Array.isArray(opts.base) && opts.base.length > 1
54
54
  const undiciOpts = opts.undici || {}
55
55
  const globalAgent = opts.globalAgent
56
- const destroyAgent = opts.destroyAgent
56
+ const destroyAgent = opts.destroyAgent || false
57
57
  let http2Client
58
58
  let undiciAgent
59
59
  let undiciInstance
@@ -104,6 +104,11 @@ function buildRequest (opts) {
104
104
  }
105
105
 
106
106
  function close () {
107
+ // Always destroy http2 client (internal implementation detail)
108
+ if (http2Client) {
109
+ http2Client.destroy()
110
+ }
111
+
107
112
  if (globalAgent || destroyAgent === false) {
108
113
  return
109
114
  }
@@ -114,18 +119,28 @@ function buildRequest (opts) {
114
119
  } else if (!isHttp2) {
115
120
  agents['http:'].destroy()
116
121
  agents['https:'].destroy()
117
- } else if (http2Client) {
118
- http2Client.destroy()
119
122
  }
120
123
  }
121
124
 
122
125
  function handleHttp1Req (opts, done) {
126
+ // Strip Connection header and headers listed in it (RFC 7230 Section 6.1)
127
+ // Headers are already lowercased by Node.js
128
+ const connectionHeaderNames = getConnectionHeaders(opts.headers)
129
+ let headers = opts.headers
130
+ if (opts.headers.connection || connectionHeaderNames.length > 0) {
131
+ headers = { ...opts.headers }
132
+ delete headers.connection
133
+ for (let i = 0; i < connectionHeaderNames.length; i++) {
134
+ delete headers[connectionHeaderNames[i]]
135
+ }
136
+ }
137
+
123
138
  const req = requests[opts.url.protocol].request({
124
139
  method: opts.method,
125
140
  port: opts.url.port,
126
141
  path: opts.url.pathname + opts.qs,
127
142
  hostname: opts.url.hostname,
128
- headers: opts.headers,
143
+ headers,
129
144
  agent: agents[opts.url.protocol.replace(/^unix:/, '')],
130
145
  ...httpOpts.requestOptions,
131
146
  timeout: opts.timeout ?? httpOpts.requestOptions.timeout
@@ -171,10 +186,19 @@ function buildRequest (opts) {
171
186
  pool = undiciAgent
172
187
  }
173
188
 
189
+ // Strip headers listed in Connection header (RFC 7230 Section 6.1)
190
+ // Headers are already lowercased
191
+ const connectionHeaderNames = getConnectionHeaders(req.headers)
192
+
174
193
  // remove forbidden headers
175
194
  req.headers.connection = undefined
176
195
  req.headers['transfer-encoding'] = undefined
177
196
 
197
+ // Also remove headers listed in Connection header
198
+ for (let i = 0; i < connectionHeaderNames.length; i++) {
199
+ req.headers[connectionHeaderNames[i]] = undefined
200
+ }
201
+
178
202
  pool.request(req, function (err, res) {
179
203
  if (err) {
180
204
  done(err)
package/lib/utils.js CHANGED
@@ -28,10 +28,36 @@ function copyHeaders (headers, reply) {
28
28
  }
29
29
  }
30
30
 
31
+ // Parse Connection header and return list of header names to strip (per RFC 7230 Section 6.1)
32
+ function getConnectionHeaders (headers) {
33
+ const connectionHeader = headers.connection
34
+ if (typeof connectionHeader !== 'string') {
35
+ return []
36
+ }
37
+ // Connection header is comma-separated list of header names
38
+ const lowerCased = connectionHeader.toLowerCase()
39
+ const result = []
40
+ let start = 0
41
+ let end = 0
42
+ for (; end <= lowerCased.length; end++) {
43
+ if (lowerCased.charCodeAt(end) === 44 || end === lowerCased.length) { // 44 = ','
44
+ const token = lowerCased.slice(start, end).trim()
45
+ if (token.length > 0) {
46
+ result.push(token)
47
+ }
48
+ start = end + 1
49
+ }
50
+ }
51
+ return result
52
+ }
53
+
31
54
  function stripHttp1ConnectionHeaders (headers) {
32
55
  const headersKeys = Object.keys(headers)
33
56
  const dest = {}
34
57
 
58
+ // Get headers listed in Connection header that should be stripped (RFC 7230 Section 6.1)
59
+ const connectionHeaderNames = getConnectionHeaders(headers)
60
+
35
61
  let header
36
62
  let i
37
63
 
@@ -54,7 +80,10 @@ function stripHttp1ConnectionHeaders (headers) {
54
80
  }
55
81
  break
56
82
  default:
57
- dest[header] = headers[header]
83
+ // Also skip headers listed in Connection header (RFC 7230 Section 6.1)
84
+ if (!connectionHeaderNames.includes(header)) {
85
+ dest[header] = headers[header]
86
+ }
58
87
  break
59
88
  }
60
89
  }
@@ -96,6 +125,7 @@ function buildURL (source, reqBase) {
96
125
  module.exports = {
97
126
  copyHeaders,
98
127
  stripHttp1ConnectionHeaders,
128
+ getConnectionHeaders,
99
129
  filterPseudoHeaders,
100
130
  buildURL
101
131
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fastify/reply-from",
3
- "version": "12.5.0",
3
+ "version": "12.6.1",
4
4
  "description": "forward your HTTP request to another server, for fastify",
5
5
  "main": "index.js",
6
6
  "type": "commonjs",
@@ -60,10 +60,10 @@
60
60
  "devDependencies": {
61
61
  "@fastify/formbody": "^8.0.0",
62
62
  "@fastify/multipart": "^9.0.0",
63
- "@sinonjs/fake-timers": "^14.0.0",
64
- "@types/node": "^24.0.8",
63
+ "@sinonjs/fake-timers": "^15.0.0",
64
+ "@types/node": "^25.0.3",
65
65
  "@types/tap": "^18.0.0",
66
- "c8": "^10.1.3",
66
+ "c8": "^11.0.0",
67
67
  "eslint": "^9.17.0",
68
68
  "fastify": "^5.0.0",
69
69
  "form-data": "^4.0.0",
@@ -9,14 +9,14 @@ const http = require('node:http')
9
9
  const instance = Fastify()
10
10
 
11
11
  t.test('core with path in base', async (t) => {
12
- t.plan(8)
12
+ t.plan(7)
13
13
  t.after(() => instance.close())
14
14
 
15
15
  const target = http.createServer((req, res) => {
16
16
  t.assert.ok('request proxied')
17
17
  t.assert.strictEqual(req.method, 'GET')
18
18
  t.assert.strictEqual(req.url, '/hello')
19
- t.assert.strictEqual(req.headers.connection, 'close')
19
+ // Connection header is not forwarded per RFC 7230 Section 6.1
20
20
  res.statusCode = 205
21
21
  res.setHeader('Content-Type', 'text/plain')
22
22
  res.setHeader('x-my-header', 'hello!')
@@ -0,0 +1,41 @@
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('queryString empty object should not append trailing ?', async (t) => {
12
+ t.plan(4)
13
+ t.after(() => instance.close())
14
+
15
+ const target = http.createServer((req, res) => {
16
+ t.assert.ok('request proxied')
17
+ t.assert.strictEqual(req.url, '/world')
18
+ res.statusCode = 200
19
+ res.setHeader('Content-Type', 'text/plain')
20
+ res.end('hello world')
21
+ })
22
+
23
+ instance.get('/hello', (_request, reply) => {
24
+ reply.from(`http://localhost:${target.address().port}/world`, {
25
+ queryString: {}
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
+
35
+ await new Promise(resolve => instance.listen({ port: 0 }, resolve))
36
+
37
+ const result = await request(`http://localhost:${instance.server.address().port}/hello`)
38
+
39
+ t.assert.strictEqual(result.statusCode, 200)
40
+ t.assert.strictEqual(await result.body.text(), 'hello world')
41
+ })
@@ -0,0 +1,43 @@
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('queryString function returning empty string should not append trailing ?', async (t) => {
12
+ t.plan(4)
13
+ t.after(() => instance.close())
14
+
15
+ const target = http.createServer((req, res) => {
16
+ t.assert.ok('request proxied')
17
+ t.assert.strictEqual(req.url, '/world')
18
+ res.statusCode = 200
19
+ res.setHeader('Content-Type', 'text/plain')
20
+ res.end('hello world')
21
+ })
22
+
23
+ instance.get('/hello', (_request, reply) => {
24
+ reply.from(`http://localhost:${target.address().port}/world`, {
25
+ queryString () {
26
+ return ''
27
+ }
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`)
40
+
41
+ t.assert.strictEqual(result.statusCode, 200)
42
+ t.assert.strictEqual(await result.body.text(), 'hello world')
43
+ })
@@ -0,0 +1,232 @@
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
+
8
+ // RFC 7230 Section 6.1 - Connection header handling
9
+ // A proxy MUST parse the Connection header and remove any headers listed within it
10
+
11
+ // Helper to make HTTP request with Connection header (undici doesn't allow this)
12
+ function makeRequest (port, headers) {
13
+ return new Promise((resolve, reject) => {
14
+ const req = http.request({
15
+ method: 'GET',
16
+ hostname: 'localhost',
17
+ port,
18
+ path: '/',
19
+ headers
20
+ }, (res) => {
21
+ let data = ''
22
+ res.on('data', (chunk) => { data += chunk })
23
+ res.on('end', () => resolve({ statusCode: res.statusCode, body: data }))
24
+ })
25
+ req.on('error', reject)
26
+ req.end()
27
+ })
28
+ }
29
+
30
+ t.test('strips headers listed in Connection header (undici)', async (t) => {
31
+ t.plan(4)
32
+ const instance = Fastify()
33
+ instance.register(From)
34
+
35
+ t.after(() => instance.close())
36
+
37
+ const target = http.createServer((req, res) => {
38
+ t.assert.ok('request proxied')
39
+ t.assert.strictEqual(req.headers['x-custom-header'], undefined, 'X-Custom-Header should be stripped')
40
+ res.statusCode = 200
41
+ res.setHeader('Content-Type', 'text/plain')
42
+ res.end('ok')
43
+ })
44
+
45
+ instance.get('/', (_request, reply) => {
46
+ reply.from(`http://localhost:${target.address().port}`)
47
+ })
48
+
49
+ t.after(() => target.close())
50
+
51
+ await new Promise((resolve) => instance.listen({ port: 0 }, resolve))
52
+ await new Promise((resolve) => target.listen({ port: 0 }, resolve))
53
+
54
+ const result = await makeRequest(instance.server.address().port, {
55
+ 'X-Custom-Header': 'some-value',
56
+ Connection: 'X-Custom-Header'
57
+ })
58
+
59
+ t.assert.strictEqual(result.statusCode, 200)
60
+ t.assert.strictEqual(result.body, 'ok')
61
+ })
62
+
63
+ t.test('strips multiple headers listed in Connection header (undici)', async (t) => {
64
+ t.plan(5)
65
+ const instance = Fastify()
66
+ instance.register(From)
67
+
68
+ t.after(() => instance.close())
69
+
70
+ const target = http.createServer((req, res) => {
71
+ t.assert.ok('request proxied')
72
+ t.assert.strictEqual(req.headers['x-custom-one'], undefined, 'X-Custom-One should be stripped')
73
+ t.assert.strictEqual(req.headers['x-custom-two'], undefined, 'X-Custom-Two should be stripped')
74
+ res.statusCode = 200
75
+ res.setHeader('Content-Type', 'text/plain')
76
+ res.end('ok')
77
+ })
78
+
79
+ instance.get('/', (_request, reply) => {
80
+ reply.from(`http://localhost:${target.address().port}`)
81
+ })
82
+
83
+ t.after(() => target.close())
84
+
85
+ await new Promise((resolve) => instance.listen({ port: 0 }, resolve))
86
+ await new Promise((resolve) => target.listen({ port: 0 }, resolve))
87
+
88
+ const result = await makeRequest(instance.server.address().port, {
89
+ 'X-Custom-One': 'value1',
90
+ 'X-Custom-Two': 'value2',
91
+ Connection: 'X-Custom-One, X-Custom-Two'
92
+ })
93
+
94
+ t.assert.strictEqual(result.statusCode, 200)
95
+ t.assert.strictEqual(result.body, 'ok')
96
+ })
97
+
98
+ t.test('preserves headers not listed in Connection header (undici)', async (t) => {
99
+ t.plan(5)
100
+ const instance = Fastify()
101
+ instance.register(From)
102
+
103
+ t.after(() => instance.close())
104
+
105
+ const target = http.createServer((req, res) => {
106
+ t.assert.ok('request proxied')
107
+ t.assert.strictEqual(req.headers['x-keep-header'], 'keep-me', 'X-Keep-Header should be preserved')
108
+ t.assert.strictEqual(req.headers['x-strip-header'], undefined, 'X-Strip-Header should be stripped')
109
+ res.statusCode = 200
110
+ res.setHeader('Content-Type', 'text/plain')
111
+ res.end('ok')
112
+ })
113
+
114
+ instance.get('/', (_request, reply) => {
115
+ reply.from(`http://localhost:${target.address().port}`)
116
+ })
117
+
118
+ t.after(() => target.close())
119
+
120
+ await new Promise((resolve) => instance.listen({ port: 0 }, resolve))
121
+ await new Promise((resolve) => target.listen({ port: 0 }, resolve))
122
+
123
+ const result = await makeRequest(instance.server.address().port, {
124
+ 'X-Keep-Header': 'keep-me',
125
+ 'X-Strip-Header': 'strip-me',
126
+ Connection: 'X-Strip-Header'
127
+ })
128
+
129
+ t.assert.strictEqual(result.statusCode, 200)
130
+ t.assert.strictEqual(result.body, 'ok')
131
+ })
132
+
133
+ t.test('strips headers listed in Connection header (http)', async (t) => {
134
+ t.plan(4)
135
+ const instance = Fastify()
136
+ instance.register(From, { undici: false })
137
+
138
+ t.after(() => instance.close())
139
+
140
+ const target = http.createServer((req, res) => {
141
+ t.assert.ok('request proxied')
142
+ t.assert.strictEqual(req.headers['x-custom-header'], undefined, 'X-Custom-Header should be stripped')
143
+ res.statusCode = 200
144
+ res.setHeader('Content-Type', 'text/plain')
145
+ res.end('ok')
146
+ })
147
+
148
+ instance.get('/', (_request, reply) => {
149
+ reply.from(`http://localhost:${target.address().port}`)
150
+ })
151
+
152
+ t.after(() => target.close())
153
+
154
+ await new Promise((resolve) => instance.listen({ port: 0 }, resolve))
155
+ await new Promise((resolve) => target.listen({ port: 0 }, resolve))
156
+
157
+ const result = await makeRequest(instance.server.address().port, {
158
+ 'X-Custom-Header': 'some-value',
159
+ Connection: 'X-Custom-Header'
160
+ })
161
+
162
+ t.assert.strictEqual(result.statusCode, 200)
163
+ t.assert.strictEqual(result.body, 'ok')
164
+ })
165
+
166
+ t.test('strips multiple headers listed in Connection header (http)', async (t) => {
167
+ t.plan(5)
168
+ const instance = Fastify()
169
+ instance.register(From, { undici: false })
170
+
171
+ t.after(() => instance.close())
172
+
173
+ const target = http.createServer((req, res) => {
174
+ t.assert.ok('request proxied')
175
+ t.assert.strictEqual(req.headers['x-custom-one'], undefined, 'X-Custom-One should be stripped')
176
+ t.assert.strictEqual(req.headers['x-custom-two'], undefined, 'X-Custom-Two should be stripped')
177
+ res.statusCode = 200
178
+ res.setHeader('Content-Type', 'text/plain')
179
+ res.end('ok')
180
+ })
181
+
182
+ instance.get('/', (_request, reply) => {
183
+ reply.from(`http://localhost:${target.address().port}`)
184
+ })
185
+
186
+ t.after(() => target.close())
187
+
188
+ await new Promise((resolve) => instance.listen({ port: 0 }, resolve))
189
+ await new Promise((resolve) => target.listen({ port: 0 }, resolve))
190
+
191
+ const result = await makeRequest(instance.server.address().port, {
192
+ 'X-Custom-One': 'value1',
193
+ 'X-Custom-Two': 'value2',
194
+ Connection: 'X-Custom-One, X-Custom-Two'
195
+ })
196
+
197
+ t.assert.strictEqual(result.statusCode, 200)
198
+ t.assert.strictEqual(result.body, 'ok')
199
+ })
200
+
201
+ t.test('handles Connection header with keep-alive and custom headers (undici)', async (t) => {
202
+ t.plan(4)
203
+ const instance = Fastify()
204
+ instance.register(From)
205
+
206
+ t.after(() => instance.close())
207
+
208
+ const target = http.createServer((req, res) => {
209
+ t.assert.ok('request proxied')
210
+ t.assert.strictEqual(req.headers['x-custom-header'], undefined, 'X-Custom-Header should be stripped')
211
+ res.statusCode = 200
212
+ res.setHeader('Content-Type', 'text/plain')
213
+ res.end('ok')
214
+ })
215
+
216
+ instance.get('/', (_request, reply) => {
217
+ reply.from(`http://localhost:${target.address().port}`)
218
+ })
219
+
220
+ t.after(() => target.close())
221
+
222
+ await new Promise((resolve) => instance.listen({ port: 0 }, resolve))
223
+ await new Promise((resolve) => target.listen({ port: 0 }, resolve))
224
+
225
+ const result = await makeRequest(instance.server.address().port, {
226
+ 'X-Custom-Header': 'some-value',
227
+ Connection: 'keep-alive, X-Custom-Header'
228
+ })
229
+
230
+ t.assert.strictEqual(result.statusCode, 200)
231
+ t.assert.strictEqual(result.body, 'ok')
232
+ })
@@ -27,3 +27,25 @@ test('destroyAgent false', async (t) => {
27
27
  await instance.ready()
28
28
  await instance.close()
29
29
  })
30
+
31
+ test('destroyAgent default false', async (t) => {
32
+ const mockAgent = new undici.Agent()
33
+ mockAgent.destroy = () => {
34
+ t.fail()
35
+ }
36
+ const instance = Fastify()
37
+
38
+ t.after(() => instance.close())
39
+
40
+ instance.get('/', (_request, reply) => {
41
+ reply.from()
42
+ })
43
+
44
+ instance.register(From, {
45
+ base: 'http://localhost:4242',
46
+ undici: mockAgent
47
+ })
48
+
49
+ await instance.ready()
50
+ await instance.close()
51
+ })
package/.github/stale.yml DELETED
@@ -1,21 +0,0 @@
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
package/.taprc DELETED
@@ -1,3 +0,0 @@
1
- disable-coverage: true
2
- files:
3
- - test/**/*.test.js
package/CLAUDE.md DELETED
@@ -1,66 +0,0 @@
1
- # CLAUDE.md
2
-
3
- This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
-
5
- ## Development Commands
6
-
7
- ### Testing
8
- - `npm test` - Run all tests (unit + TypeScript)
9
- - `npm run test:unit` - Run unit tests with coverage using c8 and Node.js test runner
10
- - `npm run test:typescript` - Run TypeScript definition tests using tsd
11
-
12
- ### Linting
13
- - `npm run lint` - Run ESLint to check code style
14
- - `npm run lint:fix` - Run ESLint with automatic fixes
15
- - ESLint configuration uses neostandard with TypeScript support
16
-
17
- ### Building
18
- - No build step required - this is a CommonJS library
19
-
20
- ## Architecture Overview
21
-
22
- This is a Fastify plugin for proxying HTTP requests to upstream servers. The plugin decorates the Fastify reply with a `from()` method.
23
-
24
- ### Key Components
25
-
26
- **Main Plugin (`index.js`)**
27
- - Entry point that registers the plugin using fastify-plugin
28
- - Decorates `reply.from(source, opts)` method
29
- - Handles request proxying, retries, caching, and error handling
30
- - Supports HTTP/1.1, HTTP/2, and Undici client strategies
31
-
32
- **Request Builder (`lib/request.js`)**
33
- - Factory for creating request handlers based on transport (HTTP/1.1, HTTP/2, Undici)
34
- - Handles different agent configurations (HTTP, HTTPS, Unix sockets)
35
- - Manages timeouts, connection pooling, and retry logic
36
- - Returns unified interface: `{ request, close, retryOnError }`
37
-
38
- **Utilities (`lib/utils.js`)**
39
- - `filterPseudoHeaders()` - Removes HTTP/2 pseudo-headers (starting with :)
40
- - `copyHeaders()` - Copies headers to Fastify reply
41
- - `stripHttp1ConnectionHeaders()` - Removes HTTP/1 connection-specific headers for HTTP/2
42
- - `buildURL()` - Constructs destination URLs with base path validation
43
-
44
- **Error Definitions (`lib/errors.js`)**
45
- - Standardized error types using @fastify/error
46
- - Maps different error conditions to appropriate HTTP status codes
47
- - Includes timeout, connection reset, and gateway errors
48
-
49
- ### Transport Strategies
50
-
51
- 1. **Undici (default)** - High-performance HTTP/1.1 client with connection pooling
52
- 2. **HTTP/2** - Native Node.js HTTP/2 client for modern protocols
53
- 3. **HTTP/1.1** - Node.js built-in http/https modules with custom agents
54
-
55
- ### Key Features
56
-
57
- - **Caching**: URL parsing cache using toad-cache LruMap (default 100 entries)
58
- - **Retries**: Configurable retry logic for socket hangups and 503 errors
59
- - **Headers**: Smart header rewriting for HTTP/1.1 ↔ HTTP/2 compatibility
60
- - **Body handling**: Supports streams, JSON encoding, and custom content types
61
- - **Unix sockets**: Special handling for unix+http: and unix+https: protocols
62
- - **Timeouts**: Per-request and session-level timeout configuration
63
-
64
- ### Configuration
65
-
66
- The plugin accepts extensive configuration for different transport methods, retry behavior, caching, and request/response transformation hooks. See README.md for full option documentation.