From 136d5e3df4e56687e897560b9d9502de239fcc75 Mon Sep 17 00:00:00 2001 From: yfwmaniish Date: Tue, 1 Sep 2026 13:53:39 +0530 Subject: [PATCH] fix(splat): stop mutating the shared info[SPLAT] array Both code paths in Splatter (the tokens path in _splat(), and the no-tokens path in transform()) separated "meta" from "splat" by calling .splice() on info[SPLAT] directly. Since info[SPLAT] is an array reference, and winston shallow-clones info per transport (giving each transport's format pipeline its own info object but the same info[SPLAT] array), the first transport's splice mutated that shared array in place - so every subsequent transport saw an already-truncated (or emptied) splat, and silently lost the meta winston had attached to the log call. Both call sites now extract the same values via slice() instead, which reads from the array without mutating it. Fixes winstonjs/winston#2365. --- splat.js | 14 +++++++++++--- test/splat.test.js | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/splat.js b/splat.js index 5e59e23..1684112 100644 --- a/splat.js +++ b/splat.js @@ -54,9 +54,15 @@ class Splatter { const expectedSplat = tokens.length - escapes; const extraSplat = expectedSplat - splat.length; const metas = extraSplat < 0 - ? splat.splice(extraSplat, -1 * extraSplat) + ? splat.slice(extraSplat) : []; + // `splat` may be the same array referenced by info[SPLAT], which is + // shared across every transport's format pipeline for this log call - + // mutating it (e.g. via splice) here would drop the extra metadata for + // every transport after the first, so this only ever reads from it. + const formatArgs = extraSplat < 0 ? splat.slice(0, extraSplat) : splat; + // Now that { splat } has been separated from any potential { meta }. we // can assign this to the `info` object and write it to our format stream. // If the additional metas are **NOT** objects or **LACK** enumerable properties @@ -68,7 +74,7 @@ class Splatter { } } - info.message = util.format(msg, ...splat); + info.message = util.format(msg, ...formatArgs); return info; } @@ -97,8 +103,10 @@ class Splatter { // This condition will take care of inputs with info[SPLAT] // but no tokens present if (!tokens && (splat || splat.length)) { + // Read-only: splat may be the same array referenced by info[SPLAT], + // shared across every transport's format pipeline for this log call. const metas = splat.length > 1 - ? splat.splice(0) + ? splat.slice(0) : splat; // Now that { splat } has been separated from any potential { meta }. we diff --git a/test/splat.test.js b/test/splat.test.js index 24b5dff..bfdfeec 100644 --- a/test/splat.test.js +++ b/test/splat.test.js @@ -155,4 +155,37 @@ describe('splat', () => { } )); + // See: https://github.com/winstonjs/winston/issues/2365 + // winston shallow-clones info per transport, so each transport's format + // pipeline gets its own info object but the same info[SPLAT] array + // reference. Mutating that array in place during the first transport's + // pass silently drops the meta for every subsequent transport. + it('does not mutate info[SPLAT], so a second transport still sees the meta', () => { + const sharedSplat = ['ball', 'cool', { extraData: 'traceId' }]; + const infoA = { level: 'info', message: 'this %s is %s', [SPLAT]: sharedSplat }; + const infoB = { level: 'info', message: 'this %s is %s', [SPLAT]: sharedSplat }; + + const first = splat().transform(infoA); + assume(first.message).equals('this ball is cool'); + assume(first.extraData).equals('traceId'); + + const second = splat().transform(infoB); + assume(second.message).equals('this ball is cool'); + assume(second.extraData).equals('traceId'); + }); + + it('no tokens | does not mutate info[SPLAT] either', () => { + const sharedSplat = [{ extraData: 'traceId' }, { more: 'meta' }]; + const infoA = { level: 'info', message: 'no tokens here', [SPLAT]: sharedSplat }; + const infoB = { level: 'info', message: 'no tokens here', [SPLAT]: sharedSplat }; + + const first = splat().transform(infoA); + assume(first.extraData).equals('traceId'); + assume(first.more).equals('meta'); + + const second = splat().transform(infoB); + assume(second.extraData).equals('traceId'); + assume(second.more).equals('meta'); + }); + });