Skip to content
This repository was archived by the owner on Aug 4, 2023. It is now read-only.

Commit 6c8d141

Browse files
authored
fix: put [5s, 1d] range guard on the delay before re-fetching central config (#185)
Refs: elastic/apm-agent-nodejs#2941
1 parent db20708 commit 6c8d141

File tree

4 files changed

+92
-4
lines changed

4 files changed

+92
-4
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# elastic-apm-http-client changelog
22

3+
## Unreleased
4+
5+
- Add guards to ensure that a crazy `Cache-Control: max-age=...` response
6+
header cannot accidentally result in inappropriate intervals for fetching
7+
central config. The re-fetch delay is clamped to `[5 seconds, 1 day]`.
8+
(https://github.com/elastic/apm-agent-nodejs/issues/2941)
9+
310
## v11.0.1
411

512
- Fix an issue when running in a Lambda function, where a missing or erroring

index.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const StreamChopper = require('stream-chopper')
2525
const ndjson = require('./lib/ndjson')
2626
const { NoopLogger } = require('./lib/logging')
2727
const truncate = require('./lib/truncate')
28+
const { getCentralConfigIntervalS } = require('./lib/central-config')
2829

2930
module.exports = Client
3031

@@ -432,12 +433,11 @@ Client.prototype._pollConfig = function () {
432433
Client.prototype._scheduleNextConfigPoll = function (seconds) {
433434
if (this._configTimer !== null) return
434435

435-
seconds = seconds || 300
436-
436+
const delayS = getCentralConfigIntervalS(seconds)
437437
this._configTimer = setTimeout(() => {
438438
this._configTimer = null
439439
this._pollConfig()
440-
}, seconds * 1000)
440+
}, delayS * 1000)
441441

442442
this._configTimer.unref()
443443
}
@@ -1464,9 +1464,10 @@ function normalizeGlobalLabels (labels) {
14641464
return result
14651465
}
14661466

1467+
// https://httpwg.org/specs/rfc9111.html#cache-response-directive.max-age
14671468
function getMaxAge (res) {
14681469
const header = res.headers['cache-control']
1469-
const match = header && header.match(/max-age=(\d+)/)
1470+
const match = header && header.match(/max-age=(\d+)/i)
14701471
return parseInt(match && match[1], 10)
14711472
}
14721473

lib/central-config.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
'use strict'
2+
3+
// Central config-related utilities for the APM http client.
4+
5+
const INTERVAL_DEFAULT_S = 300 // 5 min
6+
const INTERVAL_MIN_S = 5
7+
const INTERVAL_MAX_S = 86400 // 1d
8+
9+
/**
10+
* Determine an appropriate delay until the next fetch of Central Config.
11+
* Default to 5 minutes, minimum 5s, max 1d.
12+
*
13+
* The maximum of 1d ensures we don't get surprised by an overflow value to
14+
* `setTimeout` per https://developer.mozilla.org/en-US/docs/Web/API/setTimeout#maximum_delay_value
15+
*
16+
* @param {Number|undefined} seconds - A number of seconds, typically pulled
17+
* from a `Cache-Control: max-age=${seconds}` header on a previous central
18+
* config request.
19+
* @returns {Number}
20+
*/
21+
function getCentralConfigIntervalS (seconds) {
22+
if (typeof seconds !== 'number' || seconds <= 0) {
23+
return INTERVAL_DEFAULT_S
24+
}
25+
return Math.min(Math.max(seconds, INTERVAL_MIN_S), INTERVAL_MAX_S)
26+
}
27+
28+
module.exports = {
29+
getCentralConfigIntervalS,
30+
31+
// These are exported for testing.
32+
INTERVAL_DEFAULT_S,
33+
INTERVAL_MIN_S,
34+
INTERVAL_MAX_S
35+
}

test/central-config.test.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,53 @@
33
const test = require('tape')
44

55
const { APMServer, validOpts, assertConfigReq } = require('./lib/utils')
6+
const {
7+
getCentralConfigIntervalS,
8+
INTERVAL_DEFAULT_S,
9+
INTERVAL_MIN_S,
10+
INTERVAL_MAX_S
11+
} = require('../lib/central-config')
612
const Client = require('../')
713

14+
test('getCentralConfigIntervalS', function (t) {
15+
const testCases = [
16+
// [ <input arg>, <expected result> ]
17+
[-4, INTERVAL_DEFAULT_S],
18+
[-1, INTERVAL_DEFAULT_S],
19+
[0, 300],
20+
[1, INTERVAL_MIN_S],
21+
[2, INTERVAL_MIN_S],
22+
[3, INTERVAL_MIN_S],
23+
[4, INTERVAL_MIN_S],
24+
[5, INTERVAL_MIN_S],
25+
[6, 6],
26+
[7, 7],
27+
[8, 8],
28+
[9, 9],
29+
[10, 10],
30+
[86398, 86398],
31+
[86399, 86399],
32+
[86400, 86400],
33+
[86401, INTERVAL_MAX_S],
34+
[86402, INTERVAL_MAX_S],
35+
[86403, INTERVAL_MAX_S],
36+
[86404, INTERVAL_MAX_S],
37+
[null, INTERVAL_DEFAULT_S],
38+
[undefined, INTERVAL_DEFAULT_S],
39+
[false, INTERVAL_DEFAULT_S],
40+
[true, INTERVAL_DEFAULT_S],
41+
['a string', INTERVAL_DEFAULT_S],
42+
[{}, INTERVAL_DEFAULT_S],
43+
[[], INTERVAL_DEFAULT_S]
44+
]
45+
46+
testCases.forEach(testCase => {
47+
t.equal(getCentralConfigIntervalS(testCase[0]), testCase[1],
48+
`getCentralConfigIntervalS(${testCase[0]}) -> ${testCase[1]}`)
49+
})
50+
t.end()
51+
})
52+
853
test('central config disabled', function (t) {
954
const origPollConfig = Client.prototype._pollConfig
1055
Client.prototype._pollConfig = function () {

0 commit comments

Comments
 (0)