From ccfbe29d85654990f78b7afa3f28e8a8fa7c2abb Mon Sep 17 00:00:00 2001 From: Kyriakos Lesgidis Date: Tue, 28 Jul 2026 18:51:17 +0300 Subject: [PATCH] Fix unbound ctx.onerror on streamed response Counter (PROD-84255) Client disconnect during a streamed response (no Content-Length) caused Counter to emit error with riviere's unbound ctx.onerror, throwing inside Koa's error handler and crashing the process. Bind the handler and add a regression test. Bump to 1.22.1. Co-authored-by: Cursor --- index.js | 2 +- package-lock.json | 4 ++-- package.json | 2 +- test/indexTest.js | 41 ++++++++++++++++++++++++++++++++++++++++- 4 files changed, 44 insertions(+), 5 deletions(-) diff --git a/index.js b/index.js index 406942b..de55819 100644 --- a/index.js +++ b/index.js @@ -67,7 +67,7 @@ function buildRiviere(options = {}) { let counter; if (!length && ctx.body && ctx.body.readable) { - ctx.body = ctx.body.pipe((counter = Counter())).on('error', ctx.onerror); + ctx.body = ctx.body.pipe((counter = Counter())).on('error', err => ctx.onerror(err)); } const res = ctx.res; diff --git a/package-lock.json b/package-lock.json index 0fbdc0d..a5e052a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@workablehr/riviere", - "version": "1.22.0", + "version": "1.22.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@workablehr/riviere", - "version": "1.22.0", + "version": "1.22.1", "license": "MIT", "dependencies": { "chalk": "^2.4.2", diff --git a/package.json b/package.json index 7baa5c8..cd73e17 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@workablehr/riviere", - "version": "1.22.0", + "version": "1.22.1", "description": "log inbound/outbound HTTP traffic", "types": "./types/riviere.d.ts", "main": "index.js", diff --git a/test/indexTest.js b/test/indexTest.js index b1a5570..cf12038 100644 --- a/test/indexTest.js +++ b/test/indexTest.js @@ -116,7 +116,8 @@ describe('riviere', () => { response: {}, body: readable, res: writable, - onerror: () => {} + // Non-arrow so a binding regression would surface as wrong `this` + onerror: function() {} }; const next = () => {}; @@ -133,4 +134,42 @@ describe('riviere', () => { await end; ctx.state.calculatedContentLength.should.equal(26); }); + + it('should call ctx.onerror bound to ctx when streamed body errors', async function() { + const readable = new Readable({ read() {} }); + const writable = new Writable({ write() {} }); + + const middleware = riviere({ + logger: { + info: sandbox.spy() + } + }); + + const streamError = new Error('client disconnect'); + const onerror = sandbox.spy(function(err) { + // Mimic Koa's Context#onerror which uses `this.app.emit(...)` + this.app.emit('error', err, this); + }); + + const ctx = { + state: {}, + response: {}, + body: readable, + res: writable, + app: { + emit: sandbox.spy() + }, + onerror + }; + + await middleware(ctx, () => {}); + + ctx.body.emit('error', streamError); + + onerror.calledOnce.should.equal(true); + onerror.calledWith(streamError).should.equal(true); + onerror.thisValues[0].should.equal(ctx); + ctx.app.emit.calledOnce.should.equal(true); + ctx.app.emit.calledWith('error', streamError, ctx).should.equal(true); + }); });