Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 20 additions & 3 deletions lib/default-hooks.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
'use strict'

const pump = require('pump')
const toArray = require('stream-to-array')
const TRANSFER_ENCODING_HEADER_NAME = 'transfer-encoding'

// Maximum size in bytes for buffered chunked responses (Connection: close)
// Prevents memory exhaustion from unbounded upstream response bodies
const MAX_BUFFER_SIZE = 1 * 1024 * 1024 // 1MB

module.exports = {
websocket: {
onOpenNoOp (ws, searchParams) {}
Expand Down Expand Up @@ -38,8 +41,22 @@ module.exports = {
}

if (!stream.headers['content-length']) {
// pack all pieces into 1 buffer to calculate content length
const resBuffer = Buffer.concat(await toArray(stream))
// Collect chunks with size limit to prevent OOM
const chunks = []
let totalSize = 0

for await (const chunk of stream) {
totalSize += chunk.length
if (totalSize > MAX_BUFFER_SIZE) {
stream.destroy()
res.statusCode = 502
res.end('Response body exceeds maximum allowed size')
return
}
chunks.push(chunk)
}

const resBuffer = Buffer.concat(chunks)

// add content-length header and send the merged response buffer
res.setHeader('content-length', '' + Buffer.byteLength(resBuffer))
Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
},
"homepage": "https://github.com/jkyberneees/fast-gateway#readme",
"dependencies": {
"fast-proxy-lite": "^1.1.2",
"fast-proxy-lite": "^1.1.3",
"http-cache-middleware": "^1.4.1",
"micromatch": "^4.0.8",
"restana": "^6.0.0",
Expand All @@ -42,8 +42,8 @@
"LICENSE"
],
"devDependencies": {
"@types/node": "^25.7.0",
"@types/express": "^5.0.6",
"@types/node": "^25.7.0",
"artillery": "^2.0.31",
"aws-sdk": "^2.1693.0",
"chai": "^6.2.2",
Expand All @@ -64,4 +64,4 @@
"response-time": "^2.3.4",
"supertest": "^7.2.2"
}
}
}
21 changes: 21 additions & 0 deletions test/smoke.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ describe('API Gateway', () => {
res.write('1')
res.end()
})
remote.get('/large-chunked', (req, res) => {
// Sends a 2MB chunked response to test buffer overflow protection
const buf = Buffer.alloc(2 * 1024 * 1024, 'x')
res.writeHead(200, { 'content-type': 'application/octet-stream' })
res.write(buf)
res.end()
})
remote.get('/cache', (req, res) => {
res.setHeader('x-cache-timeout', '1 second')
res.send({
Expand Down Expand Up @@ -369,6 +376,20 @@ describe('API Gateway', () => {
})
})

it('large chunked rejected with 502 when Connection close (buffer limit)', async () => {
await request(gateway)
.get('/users/large-chunked')
.set({ Connection: 'close' })
.expect(502)
})

it('large chunked streamed normally when Connection keep-alive', async () => {
await request(gateway)
.get('/users/large-chunked')
.set('Connection', 'keep-alive')
.expect(200)
})

it('(Should overwrite query string using req.query) GET /qs - 200', async () => {
await request(gateway)
.get('/qs?name=nodejs&category=js')
Expand Down
Loading