@fastify/reply-from 12.6.2 → 12.6.3

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.
@@ -2,12 +2,52 @@ version: 2
2
2
  updates:
3
3
  - package-ecosystem: "github-actions"
4
4
  directory: "/"
5
+ commit-message:
6
+ # Prefix all commit messages with "chore: "
7
+ prefix: "chore"
5
8
  schedule:
6
- interval: "monthly"
9
+ interval: "weekly"
10
+ cooldown:
11
+ default-days: 7
12
+ allow:
13
+ - dependency-name: "*"
14
+ update-types:
15
+ - "version-update:semver-major"
7
16
  open-pull-requests-limit: 10
8
17
 
9
18
  - package-ecosystem: "npm"
10
19
  directory: "/"
20
+ commit-message:
21
+ # Prefix all commit messages with "chore: "
22
+ prefix: "chore"
11
23
  schedule:
12
- interval: "monthly"
24
+ interval: "weekly"
25
+ cooldown:
26
+ default-days: 7
27
+ versioning-strategy: "increase-if-necessary"
28
+ allow:
29
+ - dependency-name: "*"
30
+ update-types:
31
+ - "version-update:semver-major"
32
+ ignore:
33
+ # TODO: remove ignore until neostandard support ESLint 10
34
+ - dependency-name: "eslint"
35
+ - dependency-name: "neostandard"
36
+ - dependency-name: "@stylistic/*"
13
37
  open-pull-requests-limit: 10
38
+ groups:
39
+ # Production dependencies with breaking changes
40
+ dependencies:
41
+ dependency-type: "production"
42
+ # ESLint related dependencies
43
+ dev-dependencies-eslint:
44
+ patterns:
45
+ - "eslint"
46
+ - "neostandard"
47
+ - "@stylistic/*"
48
+ # TypeScript related dependencies
49
+ dev-dependencies-typescript:
50
+ patterns:
51
+ - "@types/*"
52
+ - "tstyche"
53
+ - "typescript"
package/README.md CHANGED
@@ -70,7 +70,7 @@ Set the base URL for all the forwarded requests.
70
70
  *String or String[]*:
71
71
 
72
72
  * **Single string** → a normal `undici.Pool` / `http.request` client is used.
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.
73
+ * **Array with ≥ 2 elements** → **[`undici.BalancedPool`](https://undici.nodejs.org/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.
76
76
 
@@ -354,7 +354,7 @@ const customRetryLogic = ({req, res, err, getDefaultRetry}: RetryDetails) => {
354
354
  ### `reply.from(source, [opts])`
355
355
 
356
356
  The plugin decorates the
357
- [`Reply`](https://fastify.dev/docs/latest/Reference/Reply)
357
+ [`Reply`](https://fastify.dev/docs/latest/Reference/Reply/)
358
358
  instance with a `from` method, which will reply to the original request
359
359
  __from the desired source__. The options allows overrides of any part of
360
360
  the request or response being sent or received to/from the source.
@@ -512,7 +512,7 @@ This library has:
512
512
  - The default value for `requestTimeout` is 10 seconds (`10000`), a value of 0 disables the timeout.
513
513
  - The default value for `sessionTimeout` is 60 seconds (`60000`), a value of 0 disables the timeout.
514
514
 
515
- When a timeout happens, [`504 Gateway Timeout`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/504)
515
+ When a timeout happens, [`504 Gateway Timeout`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/504)
516
516
  will be returned to the client.
517
517
 
518
518
  ## Compatibility with @fastify/multipart
package/index.js CHANGED
@@ -146,7 +146,7 @@ const fastifyReplyFrom = fp(function from (fastify, opts, next) {
146
146
  }
147
147
  }
148
148
 
149
- // according to https://tools.ietf.org/html/rfc2616#section-4.3
149
+ // according to https://datatracker.ietf.org/doc/html/rfc2616#section-4.3
150
150
  // fastify ignore message body when it's a GET or HEAD request
151
151
  // when proxy this request, we should reset the content-length to make it a valid http request
152
152
  // discussion: https://github.com/fastify/fastify/issues/953
@@ -218,6 +218,11 @@ const fastifyReplyFrom = fp(function from (fastify, opts, next) {
218
218
  )
219
219
  } else {
220
220
  copyHeaders(rewriteHeaders(res.headers, this.request), this)
221
+ // An HTTP/1 connection cannot be reused until its request body has
222
+ // been consumed. Close it if the upstream responds before that point.
223
+ if (!req.complete) {
224
+ this.header('connection', 'close')
225
+ }
221
226
  }
222
227
  try {
223
228
  this.code(res.statusCode)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fastify/reply-from",
3
- "version": "12.6.2",
3
+ "version": "12.6.3",
4
4
  "description": "forward your HTTP request to another server, for fastify",
5
5
  "main": "index.js",
6
6
  "type": "commonjs",
@@ -10,7 +10,7 @@
10
10
  "lint:fix": "eslint --fix",
11
11
  "test": "npm run test:unit && npm run test:typescript",
12
12
  "test:unit": "c8 node --test --test-timeout=30000",
13
- "test:typescript": "tsd"
13
+ "test:typescript": "tstyche"
14
14
  },
15
15
  "repository": {
16
16
  "type": "git",
@@ -59,10 +59,9 @@
59
59
  ],
60
60
  "devDependencies": {
61
61
  "@fastify/formbody": "^8.0.0",
62
- "@fastify/multipart": "^9.0.0",
62
+ "@fastify/multipart": "^10.0.0",
63
63
  "@sinonjs/fake-timers": "^15.0.0",
64
- "@types/node": "^25.0.3",
65
- "@types/tap": "^18.0.0",
64
+ "@types/node": "^26.0.0",
66
65
  "c8": "^11.0.0",
67
66
  "eslint": "^9.17.0",
68
67
  "fastify": "^5.0.0",
@@ -73,14 +72,14 @@
73
72
  "proxy": "^4.0.0",
74
73
  "proxyquire": "^2.1.3",
75
74
  "split2": "^4.2.0",
76
- "tsd": "^0.33.0"
75
+ "tstyche": "^7.0.0"
77
76
  },
78
77
  "dependencies": {
79
78
  "@fastify/error": "^4.0.0",
80
79
  "end-of-stream": "^1.4.4",
81
80
  "fast-content-type-parse": "^3.0.0",
82
81
  "fast-querystring": "^1.1.2",
83
- "fastify-plugin": "^5.0.1",
82
+ "fastify-plugin": "^6.0.0",
84
83
  "toad-cache": "^3.7.0",
85
84
  "undici": "^7.0.0"
86
85
  },
@@ -0,0 +1,142 @@
1
+ 'use strict'
2
+
3
+ const { once } = require('node:events')
4
+ const http = require('node:http')
5
+ const net = require('node:net')
6
+ const t = require('node:test')
7
+ const Fastify = require('fastify')
8
+ const From = require('..')
9
+
10
+ for (const [name, options] of [
11
+ ['undici', {}],
12
+ ['node:http', { undici: false }]
13
+ ]) {
14
+ t.test(`closes the downstream connection when ${name} responds before the request is complete`, async (t) => {
15
+ const target = http.createServer((_req, res) => {
16
+ res.end('early')
17
+ })
18
+ const instance = Fastify()
19
+
20
+ t.after(async () => {
21
+ instance.server.closeAllConnections()
22
+ target.closeAllConnections()
23
+ await instance.close()
24
+ if (target.listening) await new Promise(resolve => target.close(resolve))
25
+ })
26
+
27
+ await new Promise(resolve => target.listen({ port: 0 }, resolve))
28
+
29
+ instance.register(From, options)
30
+ instance.addContentTypeParser('application/octet-stream', function (_request, payload, done) {
31
+ done(null, payload)
32
+ })
33
+ instance.post('/', (_request, reply) => {
34
+ reply.from(`http://localhost:${target.address().port}`)
35
+ })
36
+ await instance.listen({ port: 0 })
37
+
38
+ const socket = net.connect(instance.server.address().port, '127.0.0.1')
39
+ t.after(() => socket.destroy())
40
+ await once(socket, 'connect')
41
+
42
+ const responsePromise = new Promise((resolve, reject) => {
43
+ let response = ''
44
+ const timeout = setTimeout(() => {
45
+ reject(new Error('downstream connection was not closed'))
46
+ }, 5000)
47
+
48
+ socket.on('data', chunk => {
49
+ response += chunk.toString('latin1')
50
+ })
51
+ socket.once('error', err => {
52
+ clearTimeout(timeout)
53
+ reject(err)
54
+ })
55
+ socket.once('close', () => {
56
+ clearTimeout(timeout)
57
+ resolve(response)
58
+ })
59
+ })
60
+
61
+ socket.write([
62
+ 'POST / HTTP/1.1',
63
+ 'Host: localhost',
64
+ 'Content-Type: application/octet-stream',
65
+ 'Content-Length: 1048576',
66
+ 'Connection: keep-alive',
67
+ '',
68
+ 'a'
69
+ ].join('\r\n'))
70
+
71
+ const response = await responsePromise
72
+ t.assert.match(response, /^HTTP\/1\.1 200 OK\r\n/)
73
+ t.assert.match(response, /\r\nconnection: close\r\n/i)
74
+ t.assert.match(response, /\r\n\r\nearly$/)
75
+ })
76
+
77
+ t.test(`keeps the downstream connection reusable after ${name} consumes the request`, async (t) => {
78
+ const target = http.createServer((req, res) => {
79
+ req.resume()
80
+ req.on('end', () => res.end('complete'))
81
+ })
82
+ const agent = new http.Agent({ keepAlive: true, maxSockets: 1 })
83
+ const instance = Fastify()
84
+
85
+ t.after(async () => {
86
+ agent.destroy()
87
+ instance.server.closeAllConnections()
88
+ target.closeAllConnections()
89
+ await instance.close()
90
+ if (target.listening) await new Promise(resolve => target.close(resolve))
91
+ })
92
+
93
+ await new Promise(resolve => target.listen({ port: 0 }, resolve))
94
+
95
+ instance.register(From, options)
96
+ instance.addContentTypeParser('application/octet-stream', function (_request, payload, done) {
97
+ done(null, payload)
98
+ })
99
+ instance.post('/', (_request, reply) => {
100
+ reply.from(`http://localhost:${target.address().port}`)
101
+ })
102
+ await instance.listen({ port: 0 })
103
+
104
+ const first = await makeRequest(instance.server.address().port, agent, 'first')
105
+ t.assert.notStrictEqual(first.headers.connection, 'close')
106
+
107
+ const second = await makeRequest(instance.server.address().port, agent, 'second')
108
+ t.assert.strictEqual(second.reusedSocket, true)
109
+ t.assert.strictEqual(second.body, 'complete')
110
+ })
111
+ }
112
+
113
+ function makeRequest (port, agent, body) {
114
+ return new Promise((resolve, reject) => {
115
+ const req = http.request({
116
+ agent,
117
+ method: 'POST',
118
+ hostname: '127.0.0.1',
119
+ port,
120
+ path: '/',
121
+ headers: {
122
+ 'content-length': Buffer.byteLength(body),
123
+ 'content-type': 'application/octet-stream'
124
+ }
125
+ }, res => {
126
+ let responseBody = ''
127
+ res.setEncoding('utf8')
128
+ res.on('data', chunk => {
129
+ responseBody += chunk
130
+ })
131
+ res.on('end', () => {
132
+ resolve({
133
+ body: responseBody,
134
+ headers: res.headers,
135
+ reusedSocket: req.reusedSocket
136
+ })
137
+ })
138
+ })
139
+ req.on('error', reject)
140
+ req.end(body)
141
+ })
142
+ }
@@ -0,0 +1,123 @@
1
+ import { expect } from 'tstyche'
2
+ import fastify, {
3
+ type FastifyReply,
4
+ type FastifyRequest,
5
+ type RawServerBase,
6
+ type RequestGenericInterface,
7
+ type RouteGenericInterface
8
+ } from 'fastify'
9
+ import * as http from 'node:http'
10
+ import { type IncomingHttpHeaders } from 'node:http2'
11
+ import * as https from 'node:https'
12
+ import { Agent, Client, Dispatcher, Pool } from 'undici'
13
+ import replyFrom, {
14
+ type FastifyReplyFromOptions,
15
+ type RawServerResponse
16
+ } from '.'
17
+
18
+ const fullOptions: FastifyReplyFromOptions = {
19
+ base: 'http://example2.com',
20
+ http: {
21
+ agentOptions: {
22
+ keepAliveMsecs: 60 * 1000,
23
+ maxFreeSockets: 2048,
24
+ maxSockets: 2048
25
+ },
26
+ requestOptions: { timeout: 1000 },
27
+ agents: {
28
+ 'http:': new http.Agent({}),
29
+ 'https:': new https.Agent({})
30
+ }
31
+ },
32
+ http2: {
33
+ sessionTimeout: 1000,
34
+ requestTimeout: 1000,
35
+ sessionOptions: { rejectUnauthorized: true },
36
+ requestOptions: { endStream: true }
37
+ },
38
+ cacheURLs: 100,
39
+ disableCache: false,
40
+ undici: {
41
+ connections: 100,
42
+ pipelining: 10,
43
+ proxy: 'http://example2.com:8080'
44
+ },
45
+ contentTypesToEncode: ['application/x-www-form-urlencoded'],
46
+ retryMethods: ['GET', 'HEAD', 'OPTIONS', 'TRACE'],
47
+ maxRetriesOn503: 10,
48
+ disableRequestLogging: false,
49
+ globalAgent: false,
50
+ destroyAgent: true
51
+ }
52
+
53
+ const app = fastify()
54
+
55
+ app.register(replyFrom)
56
+ app.register(replyFrom, {})
57
+ app.register(replyFrom, { http2: true })
58
+ app.register(replyFrom, fullOptions)
59
+ app.register(replyFrom, { undici: { proxy: new URL('http://example2.com:8080') } })
60
+ app.register(replyFrom, { undici: { proxy: { uri: 'http://example2.com:8080' } } })
61
+
62
+ app.register(replyFrom, { base: 'http://example.com', undici: new Agent() })
63
+ app.register(replyFrom, { base: 'http://example.com', undici: new Pool('http://example.com') })
64
+ app.register(replyFrom, { base: 'http://example.com', undici: new Client('http://example.com') })
65
+ app.register(replyFrom, { base: 'http://example.com', undici: new Dispatcher() })
66
+
67
+ app.get('/v1', (_request, reply) => {
68
+ expect(reply.from()).type.toBe<FastifyReply>()
69
+ })
70
+
71
+ app.get('/v3', (_request, reply) => {
72
+ reply.from('/v3', {
73
+ timeout: 1000,
74
+ body: { hello: 'world' },
75
+ rewriteRequestHeaders (req, headers) {
76
+ expect(req).type.toBe<FastifyRequest<RequestGenericInterface, RawServerBase>>()
77
+ return headers
78
+ },
79
+ getUpstream (req, base) {
80
+ expect(req).type.toBe<FastifyRequest<RequestGenericInterface, RawServerBase>>()
81
+ return base
82
+ },
83
+ onResponse (request, reply, res) {
84
+ expect(request).type.toBe<FastifyRequest<RequestGenericInterface, RawServerBase>>()
85
+ expect(reply).type.toBe<FastifyReply<RouteGenericInterface, RawServerBase>>()
86
+ expect(res).type.toBe<RawServerResponse<RawServerBase>>()
87
+ expect(res.statusCode).type.toBe<number>()
88
+ }
89
+ })
90
+ })
91
+
92
+ app.get('/http2', (_request, reply) => {
93
+ reply.from('/', {
94
+ method: 'POST',
95
+ retryDelay: ({ req, res, err, attempt, getDefaultDelay }) => {
96
+ const defaultDelay = getDefaultDelay(req, res, err, attempt)
97
+ expect(defaultDelay).type.toBe<number | null>()
98
+
99
+ if (res && res.statusCode === 500 && req.method === 'GET') {
100
+ return 300
101
+ }
102
+ return null
103
+ },
104
+ rewriteHeaders (headers) {
105
+ return headers
106
+ },
107
+ rewriteRequestHeaders (_req, headers: IncomingHttpHeaders) {
108
+ return headers
109
+ },
110
+ onError (reply: FastifyReply<RouteGenericInterface, RawServerBase>, error) {
111
+ return reply.send(error.error)
112
+ },
113
+ queryString (search, reqUrl, request) {
114
+ expect(search).type.toBe<string | undefined>()
115
+ expect(reqUrl).type.toBe<string>()
116
+ expect(request).type.toBe<FastifyRequest<RequestGenericInterface, RawServerBase>>()
117
+ return ''
118
+ },
119
+ })
120
+ })
121
+
122
+ expect<FastifyReplyFromOptions>().type.toBeAssignableFrom(fullOptions)
123
+ expect<FastifyReplyFromOptions>().type.toBeAssignableFrom({ http2: true })
@@ -1,194 +0,0 @@
1
- import fastify, { FastifyReply, FastifyRequest, RawServerBase, RequestGenericInterface, RouteGenericInterface } from 'fastify'
2
- import * as http from 'node:http'
3
- import { IncomingHttpHeaders } from 'node:http2'
4
- import * as https from 'node:https'
5
- import { AddressInfo } from 'node:net'
6
- import { expectType } from 'tsd'
7
- import { Agent, Client, Dispatcher, Pool } from 'undici'
8
- import replyFrom, { FastifyReplyFromOptions, RawServerResponse } from '..'
9
- // @ts-ignore
10
- import tap from 'tap'
11
-
12
- const fullOptions: FastifyReplyFromOptions = {
13
- base: 'http://example2.com',
14
- http: {
15
- agentOptions: {
16
- keepAliveMsecs: 60 * 1000,
17
- maxFreeSockets: 2048,
18
- maxSockets: 2048
19
- },
20
- requestOptions: {
21
- timeout: 1000
22
- },
23
- agents: {
24
- 'http:': new http.Agent({}),
25
- 'https:': new https.Agent({})
26
- }
27
- },
28
- http2: {
29
- sessionTimeout: 1000,
30
- requestTimeout: 1000,
31
- sessionOptions: {
32
- rejectUnauthorized: true
33
- },
34
- requestOptions: {
35
- endStream: true
36
- }
37
- },
38
- cacheURLs: 100,
39
- disableCache: false,
40
- undici: {
41
- connections: 100,
42
- pipelining: 10,
43
- proxy: 'http://example2.com:8080'
44
- },
45
- contentTypesToEncode: ['application/x-www-form-urlencoded'],
46
- retryMethods: ['GET', 'HEAD', 'OPTIONS', 'TRACE'],
47
- maxRetriesOn503: 10,
48
- disableRequestLogging: false,
49
- globalAgent: false,
50
- destroyAgent: true
51
- }
52
-
53
- async function main () {
54
- const server = fastify()
55
-
56
- server.register(replyFrom)
57
-
58
- server.register(replyFrom, {})
59
-
60
- server.register(replyFrom, { http2: true })
61
-
62
- server.register(replyFrom, fullOptions)
63
-
64
- server.register(replyFrom, { undici: { proxy: new URL('http://example2.com:8080') } })
65
-
66
- server.register(replyFrom, { undici: { proxy: { uri: 'http://example2.com:8080' } } })
67
-
68
- server.get('/v1', (_request, reply) => {
69
- expectType<FastifyReply>(reply.from())
70
- })
71
- server.get('/v3', (_request, reply) => {
72
- reply.from('/v3', {
73
- timeout: 1000,
74
- body: { hello: 'world' },
75
- rewriteRequestHeaders (req, headers) {
76
- expectType<FastifyRequest<RequestGenericInterface, RawServerBase>>(req)
77
- return headers
78
- },
79
- getUpstream (req, base) {
80
- expectType<FastifyRequest<RequestGenericInterface, RawServerBase>>(req)
81
- return base
82
- },
83
- onResponse (request, reply, res) {
84
- expectType<FastifyRequest<RequestGenericInterface, RawServerBase>>(request)
85
- expectType<FastifyReply<RouteGenericInterface, RawServerBase>>(reply)
86
- expectType<RawServerResponse<RawServerBase>>(res)
87
- expectType<number>(res.statusCode)
88
- }
89
- })
90
- })
91
-
92
- // http2
93
- const instance = fastify({ http2: true })
94
- // @ts-ignore
95
- tap.tearDown(instance.close.bind(instance))
96
- const target = fastify({ http2: true })
97
- // @ts-ignore
98
- tap.tearDown(target.close.bind(target))
99
- instance.get('/', (_request, reply) => {
100
- reply.from()
101
- })
102
-
103
- instance.get('/http2', (_request, reply) => {
104
- reply.from('/', {
105
- method: 'POST',
106
- retryDelay: ({ req, res, err, attempt, getDefaultDelay }) => {
107
- const defaultDelay = getDefaultDelay(req, res, err, attempt)
108
- if (defaultDelay) return defaultDelay
109
-
110
- if (res && res.statusCode === 500 && req.method === 'GET') {
111
- return 300
112
- }
113
- return null
114
- },
115
- rewriteHeaders (headers) {
116
- return headers
117
- },
118
- rewriteRequestHeaders (_req, headers: IncomingHttpHeaders) {
119
- return headers
120
- },
121
- getUpstream (_req, base) {
122
- return base
123
- },
124
- onError (reply: FastifyReply<RouteGenericInterface, RawServerBase>, error) {
125
- return reply.send(error.error)
126
- },
127
- queryString (search, reqUrl, request) {
128
- expectType<string | undefined>(search)
129
- expectType<string>(reqUrl)
130
- expectType<FastifyRequest<RequestGenericInterface, RawServerBase>>(request)
131
- return ''
132
- },
133
- })
134
- })
135
-
136
- await target.listen({ port: 0 })
137
- const port = (target.server.address() as AddressInfo).port
138
- instance.register(replyFrom, {
139
- base: `http://localhost:${port}`,
140
- http2: {
141
- sessionOptions: {
142
- rejectUnauthorized: false,
143
- },
144
- },
145
- })
146
- instance.register(replyFrom, {
147
- base: `http://localhost:${port}`,
148
- http2: true,
149
- })
150
- await instance.listen({ port: 0 })
151
-
152
- const undiciInstance = fastify()
153
- undiciInstance.register(replyFrom, {
154
- base: 'http://example2.com',
155
- undici: {
156
- pipelining: 10,
157
- connections: 10
158
- }
159
- })
160
- await undiciInstance.ready()
161
-
162
- const undiciInstanceAgent = fastify()
163
- undiciInstance.register(replyFrom, {
164
- base: 'http://example2.com',
165
- undici: new Agent()
166
- })
167
- await undiciInstanceAgent.ready()
168
-
169
- const undiciInstancePool = fastify()
170
- undiciInstance.register(replyFrom, {
171
- base: 'http://example2.com',
172
- undici: new Pool('http://example2.com')
173
- })
174
- await undiciInstancePool.ready()
175
-
176
- const undiciInstanceClient = fastify()
177
- undiciInstance.register(replyFrom, {
178
- base: 'http://example2.com',
179
- undici: new Client('http://example2.com')
180
- })
181
- await undiciInstanceClient.ready()
182
-
183
- const undiciInstanceDispatcher = fastify()
184
- undiciInstance.register(replyFrom, {
185
- base: 'http://example2.com',
186
- undici: new Dispatcher()
187
- })
188
- await undiciInstanceDispatcher.ready()
189
-
190
- tap.pass('done')
191
- tap.end()
192
- }
193
-
194
- main()