@fastify/reply-from
Version:
forward your HTTP request to another server, for fastify
127 lines (103 loc) • 3.82 kB
JavaScript
const h2url = require('h2url')
const t = require('node:test')
const Fastify = require('fastify')
const From = require('..')
const fs = require('node:fs')
const path = require('node:path')
const http2 = require('node:http2')
const certs = {
key: fs.readFileSync(path.join(__dirname, 'fixtures', 'fastify.key')),
cert: fs.readFileSync(path.join(__dirname, 'fixtures', 'fastify.cert'))
}
t.test('http2 goaway handling - reproduces issue #409', async (t) => {
let requestCount = 0
// Create a custom HTTP/2 server that sends GOAWAY after first request
const targetServer = http2.createServer()
let sessionToClose = null
targetServer.on('session', (session) => {
// Store the first session to send GOAWAY later
if (!sessionToClose) {
sessionToClose = session
}
})
targetServer.on('stream', (stream, headers) => {
requestCount++
if (requestCount === 1) {
// First request: respond normally
stream.respond({
':status': 200,
'content-type': 'application/json'
})
stream.end(JSON.stringify({ request: requestCount, message: 'first request' }))
// Send GOAWAY after response to close the HTTP/2 session gracefully
setTimeout(() => {
if (sessionToClose && !sessionToClose.destroyed) {
// Send GOAWAY with NO_ERROR to close gracefully
sessionToClose.goaway(0)
}
}, 50)
} else {
// Subsequent requests should work with a new session
stream.respond({
':status': 200,
'content-type': 'application/json'
})
stream.end(JSON.stringify({ request: requestCount, message: 'subsequent request' }))
}
})
await new Promise((resolve) => {
targetServer.listen(0, resolve)
})
const targetPort = targetServer.address().port
// Create proxy server
const instance = Fastify({
http2: true,
https: certs
})
instance.register(From, {
base: `http://localhost:${targetPort}`,
http2: true,
rejectUnauthorized: false
})
instance.get('/', (_request, reply) => {
reply.from()
})
await instance.listen({ port: 0 })
const proxyPort = instance.server.address().port
// First request - should succeed
const firstResponse = await h2url.concat({
url: `https://localhost:${proxyPort}`
})
t.assert.strictEqual(firstResponse.headers[':status'], 200)
const firstBody = JSON.parse(firstResponse.body)
t.assert.strictEqual(firstBody.request, 1)
t.assert.strictEqual(firstBody.message, 'first request')
// Wait for GOAWAY to be sent and processed
await new Promise(resolve => setTimeout(resolve, 100))
// Second request - this should fail with current implementation but work with fix
try {
const secondResponse = await h2url.concat({
url: `https://localhost:${proxyPort}`,
timeout: 1000
})
// If we get here with the current code, the request succeeded
// which means the issue might not be reproduced
t.assert.strictEqual(secondResponse.headers[':status'], 200)
const secondBody = JSON.parse(secondResponse.body)
t.assert.strictEqual(secondBody.request, 2)
t.assert.strictEqual(secondBody.message, 'subsequent request')
console.log('Second request succeeded - issue may not be reproduced or fix is already in place')
} catch (err) {
// This is expected without the fix - the session is stuck in closed state
console.log(`Second request failed (expected without fix): ${err.code || err.message}`)
// This demonstrates the issue exists - session is stuck after GOAWAY
}
// Cleanup in correct order: clients first, then proxy, then server
await instance.close()
targetServer.close()
// Force exit after a short delay to ensure test completes
setTimeout(() => {
process.exit(0)
}, 100)
})