@fastify/reply-from 12.1.0 → 12.3.0
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/.claude/settings.local.json +9 -0
- package/.github/workflows/ci.yml +3 -0
- package/LICENSE +4 -1
- package/README.md +20 -0
- package/index.js +10 -0
- package/lib/request.js +13 -3
- package/lib/utils.js +1 -0
- package/package.json +6 -8
- package/test/async-route-handler.test.js +29 -32
- package/test/balanced-pool.test.js +48 -0
- package/test/base-get.test.js +27 -30
- package/test/base-path.test.js +27 -24
- package/test/base-querystring.test.js +27 -30
- package/test/build-url.test.js +17 -15
- package/test/core-with-path-in-base.test.js +30 -33
- package/test/custom-undici-instance.test.js +28 -36
- package/test/disable-request-logging.test.js +38 -52
- package/test/fastify-multipart-incompatibility.test.js +43 -49
- package/test/fix-GHSA-v2v2-hph8-q5xp.test.js +3 -2
- package/test/full-delete-http2.test.js +12 -11
- package/test/full-get-test.test.js +27 -30
- package/test/full-https-get.test.js +30 -30
- package/test/full-post-extended-content-type.test.js +40 -44
- package/test/full-post-http2.test.js +18 -13
- package/test/full-post-stream-core.test.js +39 -43
- package/test/full-post-stream.test.js +39 -43
- package/test/full-post.test.js +36 -38
- package/test/full-querystring-rewrite-option-complex.test.js +27 -31
- package/test/full-querystring-rewrite-option-function-request.test.js +30 -36
- package/test/full-querystring-rewrite-option-function.test.js +30 -33
- package/test/full-querystring-rewrite-option.test.js +28 -31
- package/test/full-querystring-rewrite-string.test.js +27 -30
- package/test/full-querystring-rewrite.test.js +27 -30
- package/test/full-querystring.test.js +27 -30
- package/test/full-rewrite-body-content-type.test.js +37 -41
- package/test/full-rewrite-body-http.test.js +39 -43
- package/test/full-rewrite-body-to-empty-string.test.js +37 -41
- package/test/full-rewrite-body-to-null.test.js +37 -41
- package/test/full-rewrite-body.test.js +39 -43
- package/test/get-upstream-cache.test.js +12 -11
- package/test/get-upstream-http.test.js +35 -41
- package/test/get-upstream-type.test.js +24 -27
- package/test/get-upstream-undici.test.js +23 -26
- package/test/host-header.test.js +22 -18
- package/test/http-agents.test.js +27 -30
- package/test/http-global-agent.test.js +20 -33
- package/test/http-http2.test.js +13 -18
- package/test/http-invalid-target.test.js +13 -17
- package/test/http-retry.test.js +22 -29
- package/test/http-timeout.test.js +49 -42
- package/test/http2-canceled-streams-cleanup.test.js +117 -0
- package/test/http2-goaway.test.js +126 -0
- package/test/http2-http2.test.js +8 -8
- package/test/http2-https.test.js +29 -27
- package/test/http2-invalid-base.test.js +2 -2
- package/test/http2-invalid-target.test.js +13 -17
- package/test/http2-target-crash.test.js +16 -20
- package/test/http2-target-multi-crash.test.js +22 -27
- package/test/http2-timeout-disabled.test.js +20 -15
- package/test/http2-timeout.test.js +58 -65
- package/test/http2-unix-socket.test.js +2 -2
- package/test/https-agents.test.js +31 -30
- package/test/https-global-agent.test.js +22 -31
- package/test/method.test.js +37 -42
- package/test/modifyCoreObjects-false.test.js +27 -30
- package/test/no-body-opts-with-get.test.js +25 -31
- package/test/no-body-opts-with-head.test.js +28 -32
- package/test/no-stream-body-option.test.js +34 -36
- package/test/on-error.test.js +21 -23
- package/test/on-invalid-upstream-response.test.js +30 -37
- package/test/onResponse.test.js +25 -32
- package/test/padded-body.test.js +38 -43
- package/test/post-formbody.test.js +35 -40
- package/test/post-plain-text.test.js +35 -39
- package/test/post-with-custom-encoded-contenttype.test.js +37 -39
- package/test/post-with-octet-stream.test.js +18 -88
- package/test/retry-on-503.test.js +27 -26
- package/test/retry-with-a-custom-handler.test.js +33 -40
- package/test/rewrite-headers-type.test.js +24 -29
- package/test/rewrite-headers.test.js +31 -36
- package/test/rewrite-request-headers-type.test.js +25 -28
- package/test/rewrite-request-headers.test.js +26 -30
- package/test/transform-body.test.js +37 -43
- package/test/undici-agent.test.js +27 -31
- package/test/undici-body.test.js +37 -42
- package/test/undici-chaining.test.js +35 -42
- package/test/undici-connect-timeout.test.js +13 -13
- package/test/undici-custom-dispatcher.test.js +11 -11
- package/test/undici-global-agent.test.js +14 -33
- package/test/undici-no-destroy.test.js +2 -2
- package/test/undici-options.test.js +21 -24
- package/test/undici-proxy-agent.test.js +4 -3
- package/test/undici-retry.test.js +24 -31
- package/test/undici-timeout-body-partial.test.js +14 -16
- package/test/undici-timeout-body.test.js +17 -19
- package/test/undici-timeout.test.js +41 -37
- package/test/undici-with-path-in-base.test.js +28 -31
- package/test/undici.test.js +28 -31
- package/test/unexpected-error.test.js +13 -18
- package/test/unix-http-undici-from.test.js +22 -31
- package/test/unix-http-undici.test.js +32 -40
- package/test/unix-http.test.js +31 -39
- package/test/unix-https-undici.test.js +30 -35
- package/test/unix-https.test.js +35 -40
- package/types/index.d.ts +9 -2
- package/types/index.test-d.ts +3 -4
- package/test/get-with-body.test.js +0 -55
- package/test/head-with-body.test.js +0 -57
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const { test } = require('
|
|
3
|
+
const { test } = require('node:test')
|
|
4
4
|
const Fastify = require('fastify')
|
|
5
|
+
const { request, Agent } = require('undici')
|
|
5
6
|
const From = require('..')
|
|
6
|
-
const got = require('got')
|
|
7
7
|
const FakeTimers = require('@sinonjs/fake-timers')
|
|
8
8
|
|
|
9
9
|
test('http request timeout', async (t) => {
|
|
10
10
|
const clock = FakeTimers.createClock()
|
|
11
11
|
const target = Fastify()
|
|
12
|
-
t.
|
|
12
|
+
t.after(() => target.close())
|
|
13
13
|
|
|
14
14
|
target.get('/', (_request, reply) => {
|
|
15
|
-
t.
|
|
15
|
+
t.assert.ok('request arrives')
|
|
16
16
|
|
|
17
17
|
setTimeout(() => {
|
|
18
18
|
reply.status(200).send('hello world')
|
|
@@ -24,7 +24,7 @@ test('http request timeout', async (t) => {
|
|
|
24
24
|
await target.listen({ port: 0 })
|
|
25
25
|
|
|
26
26
|
const instance = Fastify()
|
|
27
|
-
t.
|
|
27
|
+
t.after(() => instance.close())
|
|
28
28
|
|
|
29
29
|
instance.register(From, { http: { requestOptions: { timeout: 100 } } })
|
|
30
30
|
|
|
@@ -34,30 +34,30 @@ test('http request timeout', async (t) => {
|
|
|
34
34
|
|
|
35
35
|
await instance.listen({ port: 0 })
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
t.equal(err.response.statusCode, 504)
|
|
41
|
-
t.match(err.response.headers['content-type'], /application\/json/)
|
|
42
|
-
t.same(JSON.parse(err.response.body), {
|
|
43
|
-
statusCode: 504,
|
|
44
|
-
code: 'FST_REPLY_FROM_GATEWAY_TIMEOUT',
|
|
45
|
-
error: 'Gateway Timeout',
|
|
46
|
-
message: 'Gateway Timeout'
|
|
37
|
+
const result = await request(`http://localhost:${instance.server.address().port}/`, {
|
|
38
|
+
dispatcher: new Agent({
|
|
39
|
+
pipelining: 0
|
|
47
40
|
})
|
|
48
|
-
|
|
49
|
-
}
|
|
41
|
+
})
|
|
50
42
|
|
|
51
|
-
t.
|
|
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
52
|
})
|
|
53
53
|
|
|
54
54
|
test('http request with specific timeout', async (t) => {
|
|
55
55
|
const clock = FakeTimers.createClock()
|
|
56
56
|
const target = Fastify()
|
|
57
|
-
t.
|
|
57
|
+
t.after(() => target.close())
|
|
58
58
|
|
|
59
59
|
target.get('/', (_request, reply) => {
|
|
60
|
-
t.
|
|
60
|
+
t.assert.ok('request arrives')
|
|
61
61
|
|
|
62
62
|
setTimeout(() => {
|
|
63
63
|
reply.status(200).send('hello world')
|
|
@@ -69,7 +69,7 @@ test('http request with specific timeout', async (t) => {
|
|
|
69
69
|
await target.listen({ port: 0 })
|
|
70
70
|
|
|
71
71
|
const instance = Fastify()
|
|
72
|
-
t.
|
|
72
|
+
t.after(() => instance.close())
|
|
73
73
|
|
|
74
74
|
instance.register(From, { http: { requestOptions: { timeout: 100 } } })
|
|
75
75
|
|
|
@@ -85,32 +85,35 @@ test('http request with specific timeout', async (t) => {
|
|
|
85
85
|
})
|
|
86
86
|
|
|
87
87
|
await instance.listen({ port: 0 })
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
try {
|
|
92
|
-
await got.get(`http://localhost:${instance.server.address().port}/fail`, { retry: 0 })
|
|
93
|
-
} catch (err) {
|
|
94
|
-
t.equal(err.response.statusCode, 504)
|
|
95
|
-
t.match(err.response.headers['content-type'], /application\/json/)
|
|
96
|
-
t.same(JSON.parse(err.response.body), {
|
|
97
|
-
statusCode: 504,
|
|
98
|
-
code: 'FST_REPLY_FROM_GATEWAY_TIMEOUT',
|
|
99
|
-
error: 'Gateway Timeout',
|
|
100
|
-
message: 'Gateway Timeout'
|
|
88
|
+
const result = await request(`http://localhost:${instance.server.address().port}/success`, {
|
|
89
|
+
dispatcher: new Agent({
|
|
90
|
+
pipelining: 0
|
|
101
91
|
})
|
|
102
|
-
|
|
103
|
-
|
|
92
|
+
})
|
|
93
|
+
t.assert.strictEqual(result.statusCode, 200)
|
|
104
94
|
|
|
105
|
-
|
|
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
|
+
})
|
|
106
109
|
})
|
|
107
110
|
|
|
108
111
|
test('http sse removes timeout test', async (t) => {
|
|
109
112
|
const target = Fastify()
|
|
110
|
-
t.
|
|
113
|
+
t.after(() => target.close())
|
|
111
114
|
|
|
112
115
|
target.get('/', (_request, reply) => {
|
|
113
|
-
t.
|
|
116
|
+
t.assert.ok('request arrives')
|
|
114
117
|
|
|
115
118
|
reply.header('content-type', 'text/event-stream').status(200).send('hello world')
|
|
116
119
|
})
|
|
@@ -118,7 +121,7 @@ test('http sse removes timeout test', async (t) => {
|
|
|
118
121
|
await target.listen({ port: 0 })
|
|
119
122
|
|
|
120
123
|
const instance = Fastify()
|
|
121
|
-
t.
|
|
124
|
+
t.after(() => instance.close())
|
|
122
125
|
|
|
123
126
|
instance.register(From, { http: { requestOptions: { timeout: 100 } } })
|
|
124
127
|
|
|
@@ -128,6 +131,10 @@ test('http sse removes timeout test', async (t) => {
|
|
|
128
131
|
|
|
129
132
|
await instance.listen({ port: 0 })
|
|
130
133
|
|
|
131
|
-
const { statusCode } = await
|
|
132
|
-
|
|
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)
|
|
133
140
|
})
|
|
@@ -0,0 +1,117 @@
|
|
|
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
|
+
})
|
|
@@ -0,0 +1,126 @@
|
|
|
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
|
+
})
|
package/test/http2-http2.test.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const h2url = require('h2url')
|
|
4
|
-
const t = require('
|
|
4
|
+
const t = require('node:test')
|
|
5
5
|
const Fastify = require('fastify')
|
|
6
6
|
const From = require('..')
|
|
7
7
|
const fs = require('node:fs')
|
|
@@ -17,14 +17,14 @@ t.test('http2 -> http2', async (t) => {
|
|
|
17
17
|
https: certs
|
|
18
18
|
})
|
|
19
19
|
|
|
20
|
-
t.
|
|
20
|
+
t.after(() => instance.close())
|
|
21
21
|
|
|
22
22
|
const target = Fastify({
|
|
23
23
|
http2: true
|
|
24
24
|
})
|
|
25
25
|
|
|
26
26
|
target.get('/', (_request, reply) => {
|
|
27
|
-
t.
|
|
27
|
+
t.assert.ok('request proxied')
|
|
28
28
|
reply.code(404).header('x-my-header', 'hello!').send({
|
|
29
29
|
hello: 'world'
|
|
30
30
|
})
|
|
@@ -34,7 +34,7 @@ t.test('http2 -> http2', async (t) => {
|
|
|
34
34
|
reply.from()
|
|
35
35
|
})
|
|
36
36
|
|
|
37
|
-
t.
|
|
37
|
+
t.after(() => target.close())
|
|
38
38
|
|
|
39
39
|
await target.listen({ port: 0 })
|
|
40
40
|
|
|
@@ -50,10 +50,10 @@ t.test('http2 -> http2', async (t) => {
|
|
|
50
50
|
url: `https://localhost:${instance.server.address().port}`
|
|
51
51
|
})
|
|
52
52
|
|
|
53
|
-
t.
|
|
54
|
-
t.
|
|
55
|
-
t.match(headers['content-type'], /application\/json/)
|
|
56
|
-
t.
|
|
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
57
|
instance.close()
|
|
58
58
|
target.close()
|
|
59
59
|
})
|
package/test/http2-https.test.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const h2url = require('h2url')
|
|
4
|
-
const t = require('
|
|
4
|
+
const t = require('node:test')
|
|
5
|
+
const assert = require('node:assert')
|
|
5
6
|
const Fastify = require('fastify')
|
|
7
|
+
const { request, Agent } = require('undici')
|
|
6
8
|
const From = require('..')
|
|
7
|
-
const got = require('got')
|
|
8
9
|
const fs = require('node:fs')
|
|
9
10
|
const path = require('node:path')
|
|
10
11
|
const certs = {
|
|
@@ -18,15 +19,12 @@ const instance = Fastify({
|
|
|
18
19
|
https: certs
|
|
19
20
|
})
|
|
20
21
|
|
|
21
|
-
t.plan(4)
|
|
22
|
-
t.teardown(instance.close.bind(instance))
|
|
23
|
-
|
|
24
22
|
const target = Fastify({
|
|
25
23
|
https: certs
|
|
26
24
|
})
|
|
27
25
|
|
|
28
26
|
target.get('/', (_request, reply) => {
|
|
29
|
-
|
|
27
|
+
assert.ok('request proxied')
|
|
30
28
|
reply.code(404).header('x-my-header', 'hello!').send({
|
|
31
29
|
hello: 'world'
|
|
32
30
|
})
|
|
@@ -36,9 +34,7 @@ instance.get('/', (_request, reply) => {
|
|
|
36
34
|
reply.from()
|
|
37
35
|
})
|
|
38
36
|
|
|
39
|
-
t
|
|
40
|
-
|
|
41
|
-
async function run () {
|
|
37
|
+
async function run (t) {
|
|
42
38
|
await target.listen({ port: 0 })
|
|
43
39
|
|
|
44
40
|
instance.register(From, {
|
|
@@ -48,33 +44,39 @@ async function run () {
|
|
|
48
44
|
|
|
49
45
|
await instance.listen({ port: 0 })
|
|
50
46
|
|
|
51
|
-
t.test('http2 -> https', async (t) => {
|
|
47
|
+
await t.test('http2 -> https', async (t) => {
|
|
48
|
+
t.plan(4)
|
|
52
49
|
const { headers, body } = await h2url.concat({
|
|
53
50
|
url: `https://localhost:${instance.server.address().port}`
|
|
54
51
|
})
|
|
55
52
|
|
|
56
|
-
t.
|
|
57
|
-
t.
|
|
58
|
-
t.match(headers['content-type'], /application\/json/)
|
|
59
|
-
t.
|
|
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' })
|
|
60
57
|
})
|
|
61
58
|
|
|
62
|
-
t.test('https -> https', async (t) => {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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: {
|
|
66
64
|
rejectUnauthorized: false
|
|
67
65
|
}
|
|
68
66
|
})
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
t.fail()
|
|
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' })
|
|
77
73
|
})
|
|
78
74
|
}
|
|
79
75
|
|
|
80
|
-
|
|
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
|
+
})
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const { test } = require('
|
|
3
|
+
const { test } = require('node:test')
|
|
4
4
|
const Fastify = require('fastify')
|
|
5
5
|
const From = require('../index')
|
|
6
6
|
|
|
7
7
|
test('http2 invalid base', async (t) => {
|
|
8
8
|
const instance = Fastify()
|
|
9
9
|
|
|
10
|
-
await t.rejects(async () => instance.register(From, {
|
|
10
|
+
await t.assert.rejects(async () => instance.register(From, {
|
|
11
11
|
http2: { requestTimeout: 100 }
|
|
12
12
|
}), new Error('Option base is required when http2 is true'))
|
|
13
13
|
})
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const { test } = require('
|
|
3
|
+
const { test } = require('node:test')
|
|
4
4
|
const Fastify = require('fastify')
|
|
5
|
+
const { request } = require('undici')
|
|
5
6
|
const From = require('..')
|
|
6
|
-
const got = require('got')
|
|
7
7
|
|
|
8
8
|
test('http2 invalid target', async (t) => {
|
|
9
9
|
const instance = Fastify()
|
|
10
10
|
|
|
11
|
-
t.
|
|
11
|
+
t.after(() => instance.close())
|
|
12
12
|
|
|
13
13
|
instance.get('/', (_request, reply) => {
|
|
14
14
|
reply.from()
|
|
@@ -20,18 +20,14 @@ test('http2 invalid target', async (t) => {
|
|
|
20
20
|
|
|
21
21
|
await instance.listen({ port: 0 })
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
})
|
|
34
|
-
return
|
|
35
|
-
}
|
|
36
|
-
t.fail()
|
|
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
|
+
})
|
|
37
33
|
})
|
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const { test } = require('
|
|
3
|
+
const { test } = require('node:test')
|
|
4
4
|
const Fastify = require('fastify')
|
|
5
|
+
const { request } = require('undici')
|
|
5
6
|
const From = require('..')
|
|
6
|
-
const got = require('got')
|
|
7
7
|
|
|
8
8
|
test('http -> http2 crash', async (t) => {
|
|
9
9
|
const instance = Fastify()
|
|
10
10
|
|
|
11
|
-
t.
|
|
11
|
+
t.after(() => instance.close())
|
|
12
12
|
|
|
13
13
|
const target = Fastify({
|
|
14
14
|
http2: true
|
|
15
15
|
})
|
|
16
16
|
|
|
17
17
|
target.get('/', (_request, reply) => {
|
|
18
|
-
t.
|
|
18
|
+
t.assert.ok('request proxied')
|
|
19
19
|
reply.code(200).send({
|
|
20
20
|
hello: 'world'
|
|
21
21
|
})
|
|
@@ -25,7 +25,7 @@ test('http -> http2 crash', async (t) => {
|
|
|
25
25
|
reply.from()
|
|
26
26
|
})
|
|
27
27
|
|
|
28
|
-
t.
|
|
28
|
+
t.after(() => target.close())
|
|
29
29
|
|
|
30
30
|
await target.listen({ port: 0 })
|
|
31
31
|
|
|
@@ -36,19 +36,15 @@ test('http -> http2 crash', async (t) => {
|
|
|
36
36
|
|
|
37
37
|
await instance.listen({ port: 0 })
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
})
|
|
51
|
-
return
|
|
52
|
-
}
|
|
53
|
-
t.fail()
|
|
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
|
+
})
|
|
54
50
|
})
|