From 5236e3f68f7927a70c88dabc1314d670b750fb13 Mon Sep 17 00:00:00 2001 From: Enoch Groot Date: Mon, 21 Sep 2026 22:49:42 +0000 Subject: [PATCH 1/2] [verified] fix: close review issues #6-#13 Pin nan 2.28.0 and c8 12.0.0; CI uses npm ci. Raise engines.node to >=22 and drop Node 18/20 from CI. Pin GitHub Actions to full SHAs. Cap Stream receive before allocate and drop buf on close. Apply kMaxContainer to pack array/map walks. Cap CLI stdin concat at MAX_STDIN_BYTES. Document a risk-based bump window in SECURITY.md. Closes #6 #7 #8 #9 #10 #11 #12 #13 --- .github/workflows/ci.yml | 25 +++++++---------- README.md | 30 ++++++++++++-------- SECURITY.md | 37 +++++++++++++++++++++++++ bin/json2msgpack | 11 +++++++- bin/msgpack2json | 11 +++++++- index.d.ts | 20 ++++++++++++-- lib/msgpack.js | 40 +++++++++++++++++++++++++-- package-lock.json | 6 ++-- package.json | 6 ++-- src/msgpack.cc | 10 +++++++ test/cli.test.js | 14 ++++++++++ test/msgpack.test.js | 59 ++++++++++++++++++++++++++++++++++++++++ test/security.test.js | 14 ++++++++++ 13 files changed, 244 insertions(+), 39 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a6cc211..d86f6cb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,41 +11,36 @@ jobs: fail-fast: false matrix: # windows-2022 ships VS 2022. windows-latest currently has VS 2026 - # (18.x), which node-gyp 10/11 bundled with Node 18/20/22 reports as + # (18.x), which node-gyp 10/11 bundled with Node 22/24 reports as # unknown version "undefined" and then fails the native rebuild. os: [ubuntu-latest, macos-latest, windows-2022] - node: [18, 20, 22] + node: [22, 24] steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: ${{ matrix.node }} - name: Install and test # bash so `&&` is a hard stop on Windows too (PowerShell would still # run npm test after a failed rebuild). shell: bash - run: npm install && npm test + run: npm ci && npm test coverage: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: - node-version: 20 - - uses: actions/setup-python@v5 + node-version: 22 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: '3.x' - name: Install gcovr run: | gcovr --version || pip install --upgrade gcovr - name: Install dependencies - run: | - if [ -f package-lock.json ]; then - npm ci || npm install - else - npm install - fi + run: npm ci # Fails the job when JS or native coverage drops below 95%. - name: Coverage run: npm run coverage diff --git a/README.md b/README.md index 3567dd4..5f77be8 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ and de-serializes JavaScript values with [MessagePack](https://msgpack.org). Packed output is a `Buffer` and is typically much smaller than JSON. -Version 2.0 requires **Node.js 18+**, vendors **msgpack-c c-7.0.2**, and +Version 2.0 requires **Node.js 22+**, vendors **msgpack-c c-7.0.2**, and rejects oversized unpack headers instead of allocating them. See [`SECURITY.md`](SECURITY.md). @@ -25,7 +25,9 @@ the buffer is a truncated (incomplete) MessagePack object. Oversized array/map/string bombs throw. A streaming helper wraps a readable socket and emits `msg`, plus `error` when -a packet cannot be unpacked (the offending buffer is dropped): +a packet cannot be unpacked or the receive buffer would exceed +`MAX_STREAM_BYTES` (the offending buffer is dropped, and the socket is +destroyed when possible): ```javascript const msgpack = require('msgpack'); @@ -71,26 +73,31 @@ successful (or attempted) unpack. Stream uses that to splice leftover data. ### Limits -* array/map length ≤ 1,000,000 +* array/map length ≤ 1,000,000 on both pack and unpack * str/bin/ext length ≤ 32 MiB * nesting depth ≤ 512 on both pack and unpack +* Stream receive buffer ≤ `MAX_STREAM_BYTES` (32 MiB + 16 bytes of framing) +* CLI stdin ≤ `MAX_STDIN_BYTES` (32 MiB) before concat/parse The payload `dd ff 00 00 00` throws `msgpack unpack limit exceeded`. Packing a -value nested deeper than 512 throws `Cowardly refusing to pack object nested -more than 512 levels deep` instead of overflowing the C stack. +sparse array or map whose length exceeds 1,000,000 throws +`msgpack pack limit exceeded`. Packing a value nested deeper than 512 throws +`Cowardly refusing to pack object nested more than 512 levels deep` instead of +overflowing the C stack. Incomplete Stream frames that would grow past +`MAX_STREAM_BYTES` throw `msgpack stream limit exceeded` before allocate. ### Building, installation, testing ``` -npm install +npm ci npm test npm run coverage ``` -Needs a C/C++ toolchain and Python (node-gyp). GitHub Actions runs Node 18/20/22 -on Ubuntu and macOS. `npm run coverage` instruments JavaScript with c8 and the -native addon with gcov, and fails under 95%. Gates and remaining uncovered -lines are documented in [`COVERAGE.md`](COVERAGE.md). +Needs a C/C++ toolchain and Python (node-gyp). GitHub Actions runs Node 22/24 +on Ubuntu, macOS, and windows-2022. `npm run coverage` instruments JavaScript +with c8 and the native addon with gcov, and fails under 95%. Gates and remaining +uncovered lines are documented in [`COVERAGE.md`](COVERAGE.md). ### Command Line Utilities @@ -126,7 +133,8 @@ echo '{"hello":"world"}' | bin/json2msgpack | bin/msgpack2json ``` `msgpack2json` prints one JSON value per line and consumes every complete -message in its input. Both exit non-zero on invalid or truncated input. +message in its input. Both exit non-zero on invalid or truncated input, and +both refuse stdin larger than `MAX_STDIN_BYTES` (32 MiB). ### Benchmarks diff --git a/SECURITY.md b/SECURITY.md index 7d3a112..087eff0 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -50,6 +50,43 @@ on circular refs / unencodable values without freeing it. The sbuffer is now owned by an RAII guard that returns pooled buffers or `msgpack_sbuffer_free`s on every exit path, including C++ exceptions. +## Stream receive buffer + +`msgpack.Stream` concatenates incomplete frames into `self.buf`. Before any +allocate, a new chunk is rejected when `self.buf.length + chunk.length` would +exceed `MAX_STREAM_BYTES` (32 MiB plus 16 bytes of MessagePack framing). The +buffer is dropped, `'error'` is emitted (`msgpack stream limit exceeded`), +and the underlying stream is `destroy()`ed when that method exists. `close`, +`end`, and `error` on the underlying stream also drop `self.buf`. + +## CLI stdin + +`bin/json2msgpack` and `bin/msgpack2json` refuse stdin larger than +`MAX_STDIN_BYTES` (32 MiB) before `Buffer.concat` / `JSON.parse` / +`unpack`. They exit 1 with `stdin exceeds MAX_STDIN_BYTES`. + +## Pack container size + +`pack()` rejects arrays and maps whose own length exceeds 1,000,000 +(`kMaxContainer`), the same policy as unpack. Sparse `Array.length` above +that cap throws `msgpack pack limit exceeded` without walking the holes. + +## Dependency bump window + +The pins below are inventory, not a calendar SLA: + +- msgpack-c **c-7.0.2** (`e17beb371b59459a13b48e166a11e123bda5bf93`) +- NAN **2.28.0** (compile-in; exact in `package.json` / shrinkwrap) + +Risk-based window: + +- **Critical or high** native advisories in vendored msgpack-c or in + compile-in NAN: bump to a reviewed fix, or document why the pin stays, + within **14 days** of that fix being available. +- **Medium**: next minor of this package. +- **Low / no advisory**: no scheduled bump. The inventory pin is not a + commitment to track upstream on a calendar. + ## License First-party code in this repository is BSD-3-Clause (see `LICENSE`). The diff --git a/bin/json2msgpack b/bin/json2msgpack index 1aa2f6e..2659fae 100755 --- a/bin/json2msgpack +++ b/bin/json2msgpack @@ -5,9 +5,18 @@ const msgpack = require('../lib/msgpack'); +const MAX_STDIN_BYTES = msgpack.MAX_STDIN_BYTES; const chunks = []; +let n = 0; -process.stdin.on('data', (d) => chunks.push(d)); +process.stdin.on('data', (d) => { + n += d.length; + if (n > MAX_STDIN_BYTES) { + console.error('json2msgpack: stdin exceeds MAX_STDIN_BYTES (' + MAX_STDIN_BYTES + ')'); + process.exit(1); + } + chunks.push(d); +}); process.stdin.on('end', () => { const text = Buffer.concat(chunks).toString('utf8'); diff --git a/bin/msgpack2json b/bin/msgpack2json index 5344a1b..f8bb21c 100755 --- a/bin/msgpack2json +++ b/bin/msgpack2json @@ -5,9 +5,18 @@ const msgpack = require('../lib/msgpack'); +const MAX_STDIN_BYTES = msgpack.MAX_STDIN_BYTES; const chunks = []; +let n = 0; -process.stdin.on('data', (d) => chunks.push(d)); +process.stdin.on('data', (d) => { + n += d.length; + if (n > MAX_STDIN_BYTES) { + console.error('msgpack2json: stdin exceeds MAX_STDIN_BYTES (' + MAX_STDIN_BYTES + ')'); + process.exit(1); + } + chunks.push(d); +}); process.stdin.on('end', () => { let buf = Buffer.concat(chunks); diff --git a/index.d.ts b/index.d.ts index f768aff..7204971 100644 --- a/index.d.ts +++ b/index.d.ts @@ -9,7 +9,8 @@ import { EventEmitter } from 'events'; * Serialize values to MessagePack. * * A single argument is packed as itself; two or more are packed as an array - * of that many elements. + * of that many elements. Arrays and maps whose length exceeds 1,000,000 + * throw `msgpack pack limit exceeded`. */ export function pack(...values: any[]): Buffer; @@ -31,11 +32,26 @@ export namespace unpack { let bytes_remaining: number; } +/** + * Cap on `Stream` receive concatenation (32 MiB plus 16 bytes of framing). + * A chunk that would take `buf.length + chunk.length` past this is rejected + * before allocate. + */ +export const MAX_STREAM_BYTES: number; + +/** + * Cap on CLI stdin accumulation in `json2msgpack` / `msgpack2json` (32 MiB). + * Overflow exits 1 before concat/parse. + */ +export const MAX_STDIN_BYTES: number; + /** * Frames MessagePack messages over a stream. * * Emits `'msg'` with each decoded value, and `'error'` if a packet cannot be - * decoded (the buffered data is then dropped). + * decoded or the receive buffer would exceed `MAX_STREAM_BYTES` (the buffered + * data is then dropped; the underlying stream is destroyed when possible). + * `buf` is also dropped on the underlying stream's `close` / `end` / `error`. */ export class Stream extends EventEmitter { constructor(s: NodeJS.ReadWriteStream); diff --git a/lib/msgpack.js b/lib/msgpack.js index a8ec5ef..6f91837 100644 --- a/lib/msgpack.js +++ b/lib/msgpack.js @@ -11,6 +11,12 @@ const mpBindings = require(__dirname + '/../build/Release/msgpackBinding'); const bpack = mpBindings.pack; const rawUnpack = mpBindings.unpack; +/* 32 MiB payload (same as unpack str/bin) plus 16 bytes of MessagePack + * framing so a max-legal bin32/str32 still fits in one Stream buffer. */ +const MAX_STREAM_BYTES = 32 * 1024 * 1024 + 16; +/* CLI stdin is capped at 32 MiB before concat/parse. */ +const MAX_STDIN_BYTES = 32 * 1024 * 1024; + /* No JS pre-pass: the binding already applies toJSON at every level and * packs Dates as ISO strings. Calling toJSON here instead turned a top-level * Buffer into Buffer.prototype.toJSON's {type,data} map rather than bin. */ @@ -30,6 +36,22 @@ function Stream(s) { const self = this; events.EventEmitter.call(self); self.buf = null; + let dead = false; + + function dropBuf() { + self.buf = null; + } + + function rejectLimit() { + if (dead) return; + dead = true; + dropBuf(); + const err = new Error('msgpack stream limit exceeded'); + self.emit('error', err); + if (typeof s.destroy === 'function') { + s.destroy(err); + } + } self.send = function (m) { const args = [pack(m)]; @@ -40,10 +62,16 @@ function Stream(s) { }; s.addListener('data', function (d) { + if (dead) return; + const have = self.buf ? self.buf.length : 0; + if (have + d.length > MAX_STREAM_BYTES) { + rejectLimit(); + return; + } if (self.buf) { - const b = buffer.Buffer.allocUnsafe(self.buf.length + d.length); - self.buf.copy(b, 0, 0, self.buf.length); - d.copy(b, self.buf.length, 0, d.length); + const b = buffer.Buffer.allocUnsafe(have + d.length); + self.buf.copy(b, 0, 0, have); + d.copy(b, have, 0, d.length); self.buf = b; } else { self.buf = d; @@ -79,6 +107,10 @@ function Stream(s) { self.emit('msg', msg); } }); + + s.addListener('close', dropBuf); + s.addListener('end', dropBuf); + s.addListener('error', dropBuf); } util.inherits(Stream, events.EventEmitter); @@ -86,3 +118,5 @@ util.inherits(Stream, events.EventEmitter); exports.pack = pack; exports.unpack = unpack; exports.Stream = Stream; +exports.MAX_STREAM_BYTES = MAX_STREAM_BYTES; +exports.MAX_STDIN_BYTES = MAX_STDIN_BYTES; diff --git a/package-lock.json b/package-lock.json index 6bac0d7..2138899 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,17 +9,17 @@ "version": "2.0.0", "license": "BSD-3-Clause", "dependencies": { - "nan": "^2.23.1" + "nan": "2.28.0" }, "bin": { "json2msgpack": "bin/json2msgpack", "msgpack2json": "bin/msgpack2json" }, "devDependencies": { - "c8": "^12.0.0" + "c8": "12.0.0" }, "engines": { - "node": ">=18" + "node": ">=22" } }, "node_modules/@bcoe/v8-coverage": { diff --git a/package.json b/package.json index 9797ffa..245168a 100644 --- a/package.json +++ b/package.json @@ -28,10 +28,10 @@ "lib": "lib" }, "engines": { - "node": ">=18" + "node": ">=22" }, "dependencies": { - "nan": "^2.23.1" + "nan": "2.28.0" }, "scripts": { "test": "node --test test/cli.test.js test/coverage-native.test.js test/msgpack.test.js test/regression.test.js test/security.test.js test/worker.test.js", @@ -48,6 +48,6 @@ "gypfile": true, "license": "BSD-3-Clause", "devDependencies": { - "c8": "^12.0.0" + "c8": "12.0.0" } } diff --git a/src/msgpack.cc b/src/msgpack.cc index 5c9c9bb..6bc4a81 100644 --- a/src/msgpack.cc +++ b/src/msgpack.cc @@ -426,7 +426,13 @@ static void PackArray(msgpack_packer* pk, v8::Local arr, int depth) { throw MsgpackException(Error("Cowardly refusing to pack circular reference")); } Mark(arr); + /* Snapshot Length() once and freeze it for the walk so a growing getter + * cannot extend the loop. Same 1e6 policy as ScanOne. */ uint32_t len = arr->Length(); + if (len > kMaxContainer) { + Unmark(arr); + throw MsgpackException(Error("msgpack pack limit exceeded")); + } /* GCOVR_EXCL_BR_START: msgpack_sbuffer_write only fails on realloc * failure, which no JS-reachable input can force. */ if (msgpack_pack_array(pk, len)) { @@ -481,6 +487,10 @@ static void PackObject(msgpack_packer* pk, v8::Local obj, int depth) throw; } uint32_t len = names->Length(); + if (len > kMaxContainer) { + Unmark(obj); + throw MsgpackException(Error("msgpack pack limit exceeded")); + } /* GCOVR_EXCL_BR_START: allocation failure only, as in PackArray. */ if (msgpack_pack_map(pk, len)) { Unmark(obj); diff --git a/test/cli.test.js b/test/cli.test.js index 1e27297..96a5cd4 100644 --- a/test/cli.test.js +++ b/test/cli.test.js @@ -68,6 +68,13 @@ describe('json2msgpack error paths', () => { assert.equal(msgpack.unpack(r.stdout), want, text); } }); + + it('exits 1 before concat when stdin exceeds MAX_STDIN_BYTES', { timeout: 30000 }, () => { + const r = run('json2msgpack', Buffer.alloc(msgpack.MAX_STDIN_BYTES + 1)); + assert.equal(r.status, 1); + assert.match(r.stderr, /json2msgpack: stdin exceeds MAX_STDIN_BYTES/); + assert.equal(r.stdout.length, 0); + }); }); describe('msgpack2json error paths', () => { @@ -121,4 +128,11 @@ describe('msgpack2json error paths', () => { assert.equal(r.status, 0); assert.deepEqual(JSON.parse(r.stdout.toString('utf8')), value); }); + + it('exits 1 before concat when stdin exceeds MAX_STDIN_BYTES', { timeout: 30000 }, () => { + const r = run('msgpack2json', Buffer.alloc(msgpack.MAX_STDIN_BYTES + 1)); + assert.equal(r.status, 1); + assert.match(r.stderr, /msgpack2json: stdin exceeds MAX_STDIN_BYTES/); + assert.equal(r.stdout.length, 0); + }); }); diff --git a/test/msgpack.test.js b/test/msgpack.test.js index 5c98aa4..01096fe 100644 --- a/test/msgpack.test.js +++ b/test/msgpack.test.js @@ -269,6 +269,65 @@ describe('msgpack.Stream', () => { assert.deepEqual(seen, ['one', 'two', 'three']); }); + function hugeChunk(n) { + return { + length: n, + copy() { + throw new Error('copy should not run'); + }, + }; + } + + it('rejects a receive concat that would exceed MAX_STREAM_BYTES before allocate', () => { + const s = new EventEmitter(); + let destroyed = 0; + s.destroy = function () { destroyed += 1; }; + const ms = new msgpack.Stream(s); + const errors = []; + ms.addListener('error', (e) => errors.push(e)); + const packed = msgpack.pack('hello'); + s.emit('data', packed.subarray(0, packed.length - 1)); + assert.ok(ms.buf); + s.emit('data', hugeChunk(msgpack.MAX_STREAM_BYTES)); + assert.equal(ms.buf, null); + assert.equal(errors.length, 1); + assert.match(errors[0].message, /stream limit exceeded/); + assert.equal(destroyed, 1); + /* Further data is ignored after the cap fires. */ + s.emit('data', msgpack.pack('ok')); + assert.equal(errors.length, 1); + }); + + it('rejects a first chunk longer than MAX_STREAM_BYTES', () => { + const s = new EventEmitter(); + let destroyed = 0; + s.destroy = function () { destroyed += 1; }; + const ms = new msgpack.Stream(s); + const errors = []; + ms.addListener('error', (e) => errors.push(e)); + s.emit('data', hugeChunk(msgpack.MAX_STREAM_BYTES + 1)); + assert.equal(ms.buf, null); + assert.equal(errors.length, 1); + assert.match(errors[0].message, /stream limit exceeded/); + assert.equal(destroyed, 1); + }); + + it('drops buf on close, end, and error of the underlying stream', () => { + const packed = msgpack.pack('hello'); + for (const ev of ['close', 'end', 'error']) { + const s = new EventEmitter(); + const ms = new msgpack.Stream(s); + s.emit('data', packed.subarray(0, packed.length - 1)); + assert.ok(ms.buf, ev); + if (ev === 'error') { + s.emit('error', new Error('socket down')); + } else { + s.emit(ev); + } + assert.equal(ms.buf, null, ev); + } + }); + it('round-trips over a TCP socket', (t, done) => { const server = net.createServer((c) => { c.write(msgpack.pack('hello ')); diff --git a/test/security.test.js b/test/security.test.js index cd408c7..492d473 100644 --- a/test/security.test.js +++ b/test/security.test.js @@ -43,6 +43,20 @@ describe('unpack DoS limits', () => { }); }); +describe('pack container limits', () => { + it('rejects a sparse array whose Length exceeds 1e6', () => { + const a = []; + a.length = 1000001; + assert.throws(() => msgpack.pack(a), /pack limit exceeded/); + }); + + it('rejects a map with more than 1e6 own keys', { timeout: 60000 }, () => { + const o = Object.create(null); + for (let i = 0; i <= 1000000; i++) o[i] = 0; + assert.throws(() => msgpack.pack(o), /pack limit exceeded/); + }); +}); + describe('pack throw paths do not leak (msgpack/msgpack-node#25686)', () => { it('survives many pack failures without crashing', () => { for (let i = 0; i < 20000; i++) { From 80e77d00565fe28283bc5975fb1e713d23e29da5 Mon Sep 17 00:00:00 2001 From: Enoch Groot Date: Mon, 21 Sep 2026 23:25:14 +0000 Subject: [PATCH 2/2] [verified] fix: re-emit underlying Stream socket errors Node treats any error listener as handling, so dropBuf on the socket swallowed ECONNRESET. Drop the buffer, then emit on Stream when !dead so rejectLimit plus destroy(err) does not double-fire. --- README.md | 4 ++-- lib/msgpack.js | 10 +++++++++- test/msgpack.test.js | 19 ++++++++++++++++--- 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 5f77be8..2895065 100644 --- a/README.md +++ b/README.md @@ -25,9 +25,9 @@ the buffer is a truncated (incomplete) MessagePack object. Oversized array/map/string bombs throw. A streaming helper wraps a readable socket and emits `msg`, plus `error` when -a packet cannot be unpacked or the receive buffer would exceed +a packet cannot be unpacked, the receive buffer would exceed `MAX_STREAM_BYTES` (the offending buffer is dropped, and the socket is -destroyed when possible): +destroyed when possible), or the underlying stream errors: ```javascript const msgpack = require('msgpack'); diff --git a/lib/msgpack.js b/lib/msgpack.js index 6f91837..5931b8f 100644 --- a/lib/msgpack.js +++ b/lib/msgpack.js @@ -110,7 +110,15 @@ function Stream(s) { s.addListener('close', dropBuf); s.addListener('end', dropBuf); - s.addListener('error', dropBuf); + /* Any 'error' listener counts as handling in Node, so this must re-emit + * on Stream. Skip when already dead: rejectLimit emits then destroy(err), + * which fires this listener again. */ + s.addListener('error', function (err) { + dropBuf(); + if (!dead) { + self.emit('error', err); + } + }); } util.inherits(Stream, events.EventEmitter); diff --git a/test/msgpack.test.js b/test/msgpack.test.js index 01096fe..219b917 100644 --- a/test/msgpack.test.js +++ b/test/msgpack.test.js @@ -281,7 +281,11 @@ describe('msgpack.Stream', () => { it('rejects a receive concat that would exceed MAX_STREAM_BYTES before allocate', () => { const s = new EventEmitter(); let destroyed = 0; - s.destroy = function () { destroyed += 1; }; + /* Node's stream.destroy(err) emits 'error' on the socket. */ + s.destroy = function (err) { + destroyed += 1; + if (err) s.emit('error', err); + }; const ms = new msgpack.Stream(s); const errors = []; ms.addListener('error', (e) => errors.push(e)); @@ -301,7 +305,10 @@ describe('msgpack.Stream', () => { it('rejects a first chunk longer than MAX_STREAM_BYTES', () => { const s = new EventEmitter(); let destroyed = 0; - s.destroy = function () { destroyed += 1; }; + s.destroy = function (err) { + destroyed += 1; + if (err) s.emit('error', err); + }; const ms = new msgpack.Stream(s); const errors = []; ms.addListener('error', (e) => errors.push(e)); @@ -317,12 +324,18 @@ describe('msgpack.Stream', () => { for (const ev of ['close', 'end', 'error']) { const s = new EventEmitter(); const ms = new msgpack.Stream(s); + const errors = []; + ms.addListener('error', (e) => errors.push(e)); s.emit('data', packed.subarray(0, packed.length - 1)); assert.ok(ms.buf, ev); if (ev === 'error') { - s.emit('error', new Error('socket down')); + const sockErr = new Error('socket down'); + s.emit('error', sockErr); + assert.equal(errors.length, 1, ev); + assert.strictEqual(errors[0], sockErr, ev); } else { s.emit(ev); + assert.equal(errors.length, 0, ev); } assert.equal(ms.buf, null, ev); }