From e9a077cf759605fd24ed19a1ad59f6f30ff54cee Mon Sep 17 00:00:00 2001 From: Hashim Khan Date: Sat, 1 Aug 2026 00:11:18 +0500 Subject: [PATCH 1/2] Fix escaping for non-string label values in exposition Coerce label values with String() before escape so quotes and newlines in array (and other non-string) values cannot break Prometheus text format, and Symbol values do not throw. Signed-off-by: Hashim Khan --- lib/registry.js | 8 +++---- test/registerTest.js | 51 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 5 deletions(-) diff --git a/lib/registry.js b/lib/registry.js index 9c9aeaf7..35d516bc 100644 --- a/lib/registry.js +++ b/lib/registry.js @@ -303,11 +303,9 @@ function flattenSharedLabels(labels) { sharedLabelCache.set(labels, flattened); return flattened; } -function escapeLabelValue(str) { - if (typeof str !== 'string') { - return str; - } - return escapeString(str).replace(/"/g, '\\"'); +function escapeLabelValue(value) { + // String() handles Symbols; template coercion throws on them + return escapeString(String(value)).replace(/"/g, '\\"'); } function escapeString(str) { return str.replace(/\\/g, '\\\\').replace(/\n/g, '\\n'); diff --git a/test/registerTest.js b/test/registerTest.js index ce4a8757..b107d172 100644 --- a/test/registerTest.js +++ b/test/registerTest.js @@ -340,6 +340,57 @@ describe('Register', () => { expect(escapedResult).toMatch(/\\"/); }); + it('should escape quotes and newlines in non-string label values', async () => { + register.registerMetric({ + async get() { + return { + name: 'test_metric', + type: 'gauge', + help: 'A test metric', + values: [ + { + value: 1, + labels: { + x: ['say "hi"'], + }, + }, + { + value: 2, + labels: { + y: ['a\nb'], + }, + }, + ], + }; + }, + }); + const escapedResult = await register.metrics(); + expect(escapedResult).toContain('x="say \\"hi\\""'); + expect(escapedResult).toContain('y="a\\nb"'); + }); + + it('should coerce Symbol label values without throwing', async () => { + register.registerMetric({ + async get() { + return { + name: 'test_metric', + type: 'gauge', + help: 'A test metric', + values: [ + { + value: 1, + labels: { + sym: Symbol('x'), + }, + }, + ], + }; + }, + }); + const escapedResult = await register.metrics(); + expect(escapedResult).toContain('sym="Symbol(x)"'); + }); + describe('should output metrics as JSON', () => { it('should output metrics as JSON', async () => { register.registerMetric(getMetric()); From d9d907583841650340709e27961076837ed03723 Mon Sep 17 00:00:00 2001 From: Hashim Khan Date: Sat, 1 Aug 2026 19:02:19 +0500 Subject: [PATCH 2/2] Fix label value escaping without string hot-path regression Coerce non-string label values with String() only when needed, and document the fix in CHANGELOG. Signed-off-by: Hashim Khan --- CHANGELOG.md | 4 ++++ lib/registry.js | 7 +++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 11be4a31..8e4e115a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ project adheres to [Semantic Versioning](http://semver.org/). This release marks our first release under the Prometheus umbrella. +### Fixed + +- Fix escaping for non-string label values without regressing string hot path + ### Breaking - Drop support for Node.js versions 16, 18, 20, 21 and 23 diff --git a/lib/registry.js b/lib/registry.js index 35d516bc..97b07205 100644 --- a/lib/registry.js +++ b/lib/registry.js @@ -304,8 +304,11 @@ function flattenSharedLabels(labels) { return flattened; } function escapeLabelValue(value) { - // String() handles Symbols; template coercion throws on them - return escapeString(String(value)).replace(/"/g, '\\"'); + // Fast-path strings; String() only for non-strings (e.g. Symbols) + if (typeof value !== 'string') { + value = String(value); + } + return escapeString(value).replace(/"/g, '\\"'); } function escapeString(str) { return str.replace(/\\/g, '\\\\').replace(/\n/g, '\\n');