Skip to content

Commit be1c0f2

Browse files
[Backport 7.x] Add support for maxResponseSize and maxCompressedResponseSize (#1553)
Co-authored-by: Tomas Della Vedova <[email protected]>
1 parent da0bfd2 commit be1c0f2

File tree

7 files changed

+265
-6
lines changed

7 files changed

+265
-6
lines changed

docs/basic-config.asciidoc

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,14 @@ _Default:_ `false`
259259
|`string` - If configured, verify that the fingerprint of the CA certificate that has signed the certificate of the server matches the supplied fingerprint. Only accepts SHA256 digest fingerprints. +
260260
_Default:_ `null`
261261

262+
|`maxResponseSize`
263+
|`number` - When configured, it verifies that the uncompressed response size is lower than the configured number, if it's higher it will abort the request. It cannot be higher than buffer.constants.MAX_STRING_LENTGH +
264+
_Default:_ `null`
265+
266+
|`maxCompressedResponseSize`
267+
|`number` - When configured, it verifies that the compressed response size is lower than the configured number, if it's higher it will abort the request. It cannot be higher than buffer.constants.MAX_LENTGH +
268+
_Default:_ `null`
269+
262270
|===
263271

264272
[discrete]

docs/connecting.asciidoc

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -418,6 +418,15 @@ _Default:_ `null`
418418
|`context`
419419
|`any` - Custom object per request. _(you can use it to pass data to the clients events)_ +
420420
_Default:_ `null`
421+
422+
|`maxResponseSize`
423+
|`number` - When configured, it verifies that the uncompressed response size is lower than the configured number, if it's higher it will abort the request. It cannot be higher than buffer.constants.MAX_STRING_LENTGH +
424+
_Default:_ `null`
425+
426+
|`maxCompressedResponseSize`
427+
|`number` - When configured, it verifies that the compressed response size is lower than the configured number, if it's higher it will abort the request. It cannot be higher than buffer.constants.MAX_LENTGH +
428+
_Default:_ `null`
429+
421430
|===
422431

423432
[discrete]

index.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,8 @@ interface ClientOptions {
119119
};
120120
disablePrototypePoisoningProtection?: boolean | 'proto' | 'constructor';
121121
caFingerprint?: string;
122+
maxResponseSize?: number;
123+
maxCompressedResponseSize?: number;
122124
}
123125

124126
declare class Client {

index.js

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
const { EventEmitter } = require('events')
2323
const { URL } = require('url')
24+
const buffer = require('buffer')
2425
const debug = require('debug')('elasticsearch')
2526
const Transport = require('./lib/Transport')
2627
const Connection = require('./lib/Connection')
@@ -114,9 +115,19 @@ class Client extends ESAPI {
114115
context: null,
115116
proxy: null,
116117
enableMetaHeader: true,
117-
disablePrototypePoisoningProtection: false
118+
disablePrototypePoisoningProtection: false,
119+
maxResponseSize: null,
120+
maxCompressedResponseSize: null
118121
}, opts)
119122

123+
if (options.maxResponseSize !== null && options.maxResponseSize > buffer.constants.MAX_STRING_LENGTH) {
124+
throw new ConfigurationError(`The maxResponseSize cannot be bigger than ${buffer.constants.MAX_STRING_LENGTH}`)
125+
}
126+
127+
if (options.maxCompressedResponseSize !== null && options.maxCompressedResponseSize > buffer.constants.MAX_LENGTH) {
128+
throw new ConfigurationError(`The maxCompressedResponseSize cannot be bigger than ${buffer.constants.MAX_LENGTH}`)
129+
}
130+
120131
if (options.caFingerprint !== null && isHttpConnection(opts.node || opts.nodes)) {
121132
throw new ConfigurationError('You can\'t configure the caFingerprint with a http connection')
122133
}
@@ -178,7 +189,9 @@ class Client extends ESAPI {
178189
generateRequestId: options.generateRequestId,
179190
name: options.name,
180191
opaqueIdPrefix: options.opaqueIdPrefix,
181-
context: options.context
192+
context: options.context,
193+
maxResponseSize: options.maxResponseSize,
194+
maxCompressedResponseSize: options.maxCompressedResponseSize
182195
})
183196

184197
this.helpers = new Helpers({

lib/Transport.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ interface TransportOptions {
6161
generateRequestId?: generateRequestIdFn;
6262
name?: string;
6363
opaqueIdPrefix?: string;
64+
maxResponseSize?: number;
65+
maxCompressedResponseSize?: number;
6466
}
6567

6668
export interface RequestEvent<TResponse = Record<string, any>, TContext = Context> {
@@ -113,6 +115,8 @@ export interface TransportRequestOptions {
113115
context?: Context;
114116
warnings?: string[];
115117
opaqueId?: string;
118+
maxResponseSize?: number;
119+
maxCompressedResponseSize?: number;
116120
}
117121

118122
export interface TransportRequestCallback {

lib/Transport.js

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ const MAX_STRING_LENGTH = buffer.constants.MAX_STRING_LENGTH
4343
const kProductCheck = Symbol('product check')
4444
const kApiVersioning = Symbol('api versioning')
4545
const kEventEmitter = Symbol('event emitter')
46+
const kMaxResponseSize = Symbol('max response size')
47+
const kMaxCompressedResponseSize = Symbol('max compressed response size')
4648

4749
class Transport {
4850
constructor (opts) {
@@ -72,6 +74,8 @@ class Transport {
7274
this[kProductCheck] = 0 // 0 = to be checked, 1 = checking, 2 = checked-ok, 3 checked-notok, 4 checked-nodefault
7375
this[kApiVersioning] = process.env.ELASTIC_CLIENT_APIVERSIONING === 'true'
7476
this[kEventEmitter] = new EventEmitter()
77+
this[kMaxResponseSize] = opts.maxResponseSize || MAX_STRING_LENGTH
78+
this[kMaxCompressedResponseSize] = opts.maxCompressedResponseSize || MAX_BUFFER_LENGTH
7579

7680
this.nodeFilter = opts.nodeFilter || defaultNodeFilter
7781
if (typeof opts.nodeSelector === 'function') {
@@ -162,6 +166,8 @@ class Transport {
162166
? 0
163167
: (typeof options.maxRetries === 'number' ? options.maxRetries : this.maxRetries)
164168
const compression = options.compression !== undefined ? options.compression : this.compression
169+
const maxResponseSize = options.maxResponseSize || this[kMaxResponseSize]
170+
const maxCompressedResponseSize = options.maxCompressedResponseSize || this[kMaxCompressedResponseSize]
165171
let request = { abort: noop }
166172
const transportReturn = {
167173
then (onFulfilled, onRejected) {
@@ -244,15 +250,15 @@ class Transport {
244250
/* istanbul ignore else */
245251
if (result.headers['content-length'] !== undefined) {
246252
const contentLength = Number(result.headers['content-length'])
247-
if (isCompressed && contentLength > MAX_BUFFER_LENGTH) {
253+
if (isCompressed && contentLength > maxCompressedResponseSize) {
248254
response.destroy()
249255
return onConnectionError(
250-
new RequestAbortedError(`The content length (${contentLength}) is bigger than the maximum allowed buffer (${MAX_BUFFER_LENGTH})`, result)
256+
new RequestAbortedError(`The content length (${contentLength}) is bigger than the maximum allowed buffer (${maxCompressedResponseSize})`, result)
251257
)
252-
} else if (contentLength > MAX_STRING_LENGTH) {
258+
} else if (contentLength > maxResponseSize) {
253259
response.destroy()
254260
return onConnectionError(
255-
new RequestAbortedError(`The content length (${contentLength}) is bigger than the maximum allowed string (${MAX_STRING_LENGTH})`, result)
261+
new RequestAbortedError(`The content length (${contentLength}) is bigger than the maximum allowed string (${maxResponseSize})`, result)
256262
)
257263
}
258264
}

test/unit/client.test.js

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1308,6 +1308,223 @@ test('Content length too big (string)', t => {
13081308
})
13091309
})
13101310

1311+
test('Content length too big custom (buffer)', t => {
1312+
t.plan(4)
1313+
1314+
class MockConnection extends Connection {
1315+
request (params, callback) {
1316+
const stream = intoStream(JSON.stringify({ hello: 'world' }))
1317+
stream.statusCode = 200
1318+
stream.headers = {
1319+
'content-type': 'application/json;utf=8',
1320+
'content-encoding': 'gzip',
1321+
'content-length': 1100,
1322+
connection: 'keep-alive',
1323+
date: new Date().toISOString()
1324+
}
1325+
stream.on('close', () => t.pass('Stream destroyed'))
1326+
process.nextTick(callback, null, stream)
1327+
return { abort () {} }
1328+
}
1329+
}
1330+
1331+
const client = new Client({
1332+
node: 'http://localhost:9200',
1333+
Connection: MockConnection,
1334+
maxCompressedResponseSize: 1000
1335+
})
1336+
client.info((err, result) => {
1337+
t.ok(err instanceof errors.RequestAbortedError)
1338+
t.equal(err.message, 'The content length (1100) is bigger than the maximum allowed buffer (1000)')
1339+
t.equal(result.meta.attempts, 0)
1340+
})
1341+
})
1342+
1343+
test('Content length too big custom (string)', t => {
1344+
t.plan(4)
1345+
1346+
class MockConnection extends Connection {
1347+
request (params, callback) {
1348+
const stream = intoStream(JSON.stringify({ hello: 'world' }))
1349+
stream.statusCode = 200
1350+
stream.headers = {
1351+
'content-type': 'application/json;utf=8',
1352+
'content-length': 1100,
1353+
connection: 'keep-alive',
1354+
date: new Date().toISOString()
1355+
}
1356+
stream.on('close', () => t.pass('Stream destroyed'))
1357+
process.nextTick(callback, null, stream)
1358+
return { abort () {} }
1359+
}
1360+
}
1361+
1362+
const client = new Client({
1363+
node: 'http://localhost:9200',
1364+
Connection: MockConnection,
1365+
maxResponseSize: 1000
1366+
})
1367+
client.info((err, result) => {
1368+
t.ok(err instanceof errors.RequestAbortedError)
1369+
t.equal(err.message, 'The content length (1100) is bigger than the maximum allowed string (1000)')
1370+
t.equal(result.meta.attempts, 0)
1371+
})
1372+
})
1373+
1374+
test('Content length too big custom option (buffer)', t => {
1375+
t.plan(4)
1376+
1377+
class MockConnection extends Connection {
1378+
request (params, callback) {
1379+
const stream = intoStream(JSON.stringify({ hello: 'world' }))
1380+
stream.statusCode = 200
1381+
stream.headers = {
1382+
'content-type': 'application/json;utf=8',
1383+
'content-encoding': 'gzip',
1384+
'content-length': 1100,
1385+
connection: 'keep-alive',
1386+
date: new Date().toISOString()
1387+
}
1388+
stream.on('close', () => t.pass('Stream destroyed'))
1389+
process.nextTick(callback, null, stream)
1390+
return { abort () {} }
1391+
}
1392+
}
1393+
1394+
const client = new Client({
1395+
node: 'http://localhost:9200',
1396+
Connection: MockConnection
1397+
})
1398+
client.info({}, { maxCompressedResponseSize: 1000 }, (err, result) => {
1399+
t.ok(err instanceof errors.RequestAbortedError)
1400+
t.equal(err.message, 'The content length (1100) is bigger than the maximum allowed buffer (1000)')
1401+
t.equal(result.meta.attempts, 0)
1402+
})
1403+
})
1404+
1405+
test('Content length too big custom option (string)', t => {
1406+
t.plan(4)
1407+
1408+
class MockConnection extends Connection {
1409+
request (params, callback) {
1410+
const stream = intoStream(JSON.stringify({ hello: 'world' }))
1411+
stream.statusCode = 200
1412+
stream.headers = {
1413+
'content-type': 'application/json;utf=8',
1414+
'content-length': 1100,
1415+
connection: 'keep-alive',
1416+
date: new Date().toISOString()
1417+
}
1418+
stream.on('close', () => t.pass('Stream destroyed'))
1419+
process.nextTick(callback, null, stream)
1420+
return { abort () {} }
1421+
}
1422+
}
1423+
1424+
const client = new Client({
1425+
node: 'http://localhost:9200',
1426+
Connection: MockConnection
1427+
})
1428+
client.info({}, { maxResponseSize: 1000 }, (err, result) => {
1429+
t.ok(err instanceof errors.RequestAbortedError)
1430+
t.equal(err.message, 'The content length (1100) is bigger than the maximum allowed string (1000)')
1431+
t.equal(result.meta.attempts, 0)
1432+
})
1433+
})
1434+
1435+
test('Content length too big custom option override (buffer)', t => {
1436+
t.plan(4)
1437+
1438+
class MockConnection extends Connection {
1439+
request (params, callback) {
1440+
const stream = intoStream(JSON.stringify({ hello: 'world' }))
1441+
stream.statusCode = 200
1442+
stream.headers = {
1443+
'content-type': 'application/json;utf=8',
1444+
'content-encoding': 'gzip',
1445+
'content-length': 1100,
1446+
connection: 'keep-alive',
1447+
date: new Date().toISOString()
1448+
}
1449+
stream.on('close', () => t.pass('Stream destroyed'))
1450+
process.nextTick(callback, null, stream)
1451+
return { abort () {} }
1452+
}
1453+
}
1454+
1455+
const client = new Client({
1456+
node: 'http://localhost:9200',
1457+
Connection: MockConnection,
1458+
maxCompressedResponseSize: 2000
1459+
})
1460+
client.info({}, { maxCompressedResponseSize: 1000 }, (err, result) => {
1461+
t.ok(err instanceof errors.RequestAbortedError)
1462+
t.equal(err.message, 'The content length (1100) is bigger than the maximum allowed buffer (1000)')
1463+
t.equal(result.meta.attempts, 0)
1464+
})
1465+
})
1466+
1467+
test('Content length too big custom option override (string)', t => {
1468+
t.plan(4)
1469+
1470+
class MockConnection extends Connection {
1471+
request (params, callback) {
1472+
const stream = intoStream(JSON.stringify({ hello: 'world' }))
1473+
stream.statusCode = 200
1474+
stream.headers = {
1475+
'content-type': 'application/json;utf=8',
1476+
'content-length': 1100,
1477+
connection: 'keep-alive',
1478+
date: new Date().toISOString()
1479+
}
1480+
stream.on('close', () => t.pass('Stream destroyed'))
1481+
process.nextTick(callback, null, stream)
1482+
return { abort () {} }
1483+
}
1484+
}
1485+
1486+
const client = new Client({
1487+
node: 'http://localhost:9200',
1488+
Connection: MockConnection,
1489+
maxResponseSize: 2000
1490+
})
1491+
client.info({}, { maxResponseSize: 1000 }, (err, result) => {
1492+
t.ok(err instanceof errors.RequestAbortedError)
1493+
t.equal(err.message, 'The content length (1100) is bigger than the maximum allowed string (1000)')
1494+
t.equal(result.meta.attempts, 0)
1495+
})
1496+
})
1497+
1498+
test('maxResponseSize cannot be bigger than buffer.constants.MAX_STRING_LENGTH', t => {
1499+
t.plan(2)
1500+
1501+
try {
1502+
new Client({ // eslint-disable-line
1503+
node: 'http://localhost:9200',
1504+
maxResponseSize: buffer.constants.MAX_STRING_LENGTH + 10
1505+
})
1506+
t.fail('should throw')
1507+
} catch (err) {
1508+
t.ok(err instanceof errors.ConfigurationError)
1509+
t.equal(err.message, `The maxResponseSize cannot be bigger than ${buffer.constants.MAX_STRING_LENGTH}`)
1510+
}
1511+
})
1512+
1513+
test('maxCompressedResponseSize cannot be bigger than buffer.constants.MAX_STRING_LENGTH', t => {
1514+
t.plan(2)
1515+
1516+
try {
1517+
new Client({ // eslint-disable-line
1518+
node: 'http://localhost:9200',
1519+
maxCompressedResponseSize: buffer.constants.MAX_LENGTH + 10
1520+
})
1521+
t.fail('should throw')
1522+
} catch (err) {
1523+
t.ok(err instanceof errors.ConfigurationError)
1524+
t.equal(err.message, `The maxCompressedResponseSize cannot be bigger than ${buffer.constants.MAX_LENGTH}`)
1525+
}
1526+
})
1527+
13111528
test('Meta header enabled', t => {
13121529
t.plan(2)
13131530

0 commit comments

Comments
 (0)