From f77d67edd52b8538af9ea94e68d66754c3e84100 Mon Sep 17 00:00:00 2001 From: PNHD <26757735+PNHD@users.noreply.github.com> Date: Wed, 2 Sep 2026 15:50:15 +0700 Subject: [PATCH 1/2] Handle parser errors without crashing --- index.ts | 23 ++++++++++++++++++++--- test/test-python-shell.ts | 12 ++++++++++++ 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/index.ts b/index.ts index 9569354..ee3e552 100644 --- a/index.ts +++ b/index.ts @@ -169,6 +169,7 @@ export class PythonShell extends EventEmitter { let self = this; let errorData = ''; + let parserError: Error; EventEmitter.call(this); options = extend({}, PythonShell.defaultOptions, options); @@ -205,7 +206,12 @@ export class PythonShell extends EventEmitter { // note that setting the encoding turns the chunk into a string stdoutSplitter.setEncoding(options.encoding || 'utf8'); this.stdout.pipe(stdoutSplitter).on('data', (chunk: string) => { - this.emit('message', self.parser(chunk)); + if (parserError) return; + try { + this.emit('message', self.parser(chunk)); + } catch (err) { + parserError = err instanceof Error ? err : new Error(String(err)); + } }); } @@ -215,7 +221,12 @@ export class PythonShell extends EventEmitter { // note that setting the encoding turns the chunk into a string stderrSplitter.setEncoding(options.encoding || 'utf8'); this.stderr.pipe(stderrSplitter).on('data', (chunk: string) => { - this.emit('stderr', self.stderrParser(chunk)); + if (parserError) return; + try { + this.emit('stderr', self.stderrParser(chunk)); + } catch (err) { + parserError = err instanceof Error ? err : new Error(String(err)); + } }); } @@ -258,7 +269,10 @@ export class PythonShell extends EventEmitter { return; let err: PythonShellError; - if (self.exitCode && self.exitCode !== 0) { + if (parserError) { + err = new PythonShellError(parserError.message); + err.stack = parserError.stack; + } else if (self.exitCode && self.exitCode !== 0) { if (errorData) { err = self.parseError(errorData); } else { @@ -266,6 +280,9 @@ export class PythonShell extends EventEmitter { 'process exited with code ' + self.exitCode, ); } + } + + if (err) { err = extend(err, { executable: pythonPath, options: pythonOptions.length ? pythonOptions : null, diff --git a/test/test-python-shell.ts b/test/test-python-shell.ts index 3d5de81..0162000 100644 --- a/test/test-python-shell.ts +++ b/test/test-python-shell.ts @@ -415,6 +415,18 @@ describe('PythonShell', function () { }) .end(done); }); + it('should report JSON parser errors through the end callback', function (done) { + let pyshell = new PythonShell('echo_text.py', { + mode: 'json', + formatter: 'text', + }); + pyshell.send('not-json').end(function (err) { + should.exist(err); + err.should.be.an.Error; + err.message.should.match(/JSON|Unexpected token/); + done(); + }); + }); it('should properly buffer partial messages', function (done) { // echo_text_with_newline_control echoes text with $'s replaced with newlines let pyshell = new PythonShell('echo_text_with_newline_control.py', { From 2037b9631453c3223a6401492ea5840527a78459 Mon Sep 17 00:00:00 2001 From: PNHD <26757735+PNHD@users.noreply.github.com> Date: Thu, 3 Sep 2026 12:24:38 +0700 Subject: [PATCH 2/2] Preserve parser and listener error semantics --- README.md | 2 +- index.ts | 18 ++++++---- test/test-python-shell.ts | 75 ++++++++++++++++++++++++++++++++++++++- 3 files changed, 87 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 96e1b69..0fce35b 100644 --- a/README.md +++ b/README.md @@ -315,7 +315,7 @@ Fires when the process has been terminated, with an error or not. #### event: `pythonError` -Fires when the process terminates with a non-zero exit code. +Fires when the process terminates with a non-zero exit code or an output parser throws before normal completion. Parser exceptions are also passed to `.end()` and reject `PythonShell.run()`. #### event: `error` diff --git a/index.ts b/index.ts index ee3e552..14418dd 100644 --- a/index.ts +++ b/index.ts @@ -207,11 +207,14 @@ export class PythonShell extends EventEmitter { stdoutSplitter.setEncoding(options.encoding || 'utf8'); this.stdout.pipe(stdoutSplitter).on('data', (chunk: string) => { if (parserError) return; + let parsedChunk: any; try { - this.emit('message', self.parser(chunk)); + parsedChunk = self.parser(chunk); } catch (err) { parserError = err instanceof Error ? err : new Error(String(err)); + return; } + this.emit('message', parsedChunk); }); } @@ -222,11 +225,14 @@ export class PythonShell extends EventEmitter { stderrSplitter.setEncoding(options.encoding || 'utf8'); this.stderr.pipe(stderrSplitter).on('data', (chunk: string) => { if (parserError) return; + let parsedChunk: any; try { - this.emit('stderr', self.stderrParser(chunk)); + parsedChunk = self.stderrParser(chunk); } catch (err) { parserError = err instanceof Error ? err : new Error(String(err)); + return; } + this.emit('stderr', parsedChunk); }); } @@ -269,10 +275,7 @@ export class PythonShell extends EventEmitter { return; let err: PythonShellError; - if (parserError) { - err = new PythonShellError(parserError.message); - err.stack = parserError.stack; - } else if (self.exitCode && self.exitCode !== 0) { + if (self.exitCode && self.exitCode !== 0) { if (errorData) { err = self.parseError(errorData); } else { @@ -280,6 +283,9 @@ export class PythonShell extends EventEmitter { 'process exited with code ' + self.exitCode, ); } + } else if (parserError) { + err = new PythonShellError(parserError.message); + err.stack = parserError.stack; } if (err) { diff --git a/test/test-python-shell.ts b/test/test-python-shell.ts index 0162000..4540d73 100644 --- a/test/test-python-shell.ts +++ b/test/test-python-shell.ts @@ -1,5 +1,5 @@ import * as should from 'should'; -import { PythonShell } from '..'; +import { NewlineTransformer, PythonShell } from '..'; import { sep, join } from 'path'; import { EOL as newline } from 'os'; import { chdir, cwd } from 'process'; @@ -427,6 +427,39 @@ describe('PythonShell', function () { done(); }); }); + it('should not treat message listener exceptions as parser errors', function (done) { + let splitter = new NewlineTransformer(); + let pyshell = new PythonShell('exit-code.py', { mode: 'text' }, splitter); + let listenerError = new Error('message listener failed'); + let thrownError: Error; + + pyshell.on('message', function () { + throw listenerError; + }); + + try { + splitter.emit('data', 'hello'); + } catch (err) { + thrownError = err; + } + + should.exist(thrownError); + thrownError.should.be.exactly(listenerError); + pyshell.end(function (err) { + should.not.exist(err); + done(); + }); + }); + it('should prefer process errors over parser errors on non-zero exit', function (done) { + PythonShell.run('echo_hi_then_error.py', { mode: 'json' }).then( + () => done('expected the process to reject'), + (err) => { + err.message.should.be.exactly('Exception: fibble-fah'); + err.stack.should.containEql('----- Python Traceback -----'); + done(); + }, + ); + }); it('should properly buffer partial messages', function (done) { // echo_text_with_newline_control echoes text with $'s replaced with newlines let pyshell = new PythonShell('echo_text_with_newline_control.py', { @@ -506,6 +539,46 @@ describe('PythonShell', function () { .send('world') .end(done); }); + it('should report stderr parser errors through the end callback', function (done) { + let pyshell = new PythonShell('stderrLogging.py', { + stderrParser: function () { + throw new Error('stderr parser failed'); + }, + }); + pyshell.end(function (err) { + should.exist(err); + err.message.should.be.exactly('stderr parser failed'); + done(); + }); + }); + it('should not treat stderr listener exceptions as parser errors', function (done) { + let splitter = new NewlineTransformer(); + let pyshell = new PythonShell( + 'exit-code.py', + { mode: 'text' }, + null, + splitter, + ); + let listenerError = new Error('stderr listener failed'); + let thrownError: Error; + + pyshell.on('stderr', function () { + throw listenerError; + }); + + try { + splitter.emit('data', 'hello'); + } catch (err) { + thrownError = err; + } + + should.exist(thrownError); + thrownError.should.be.exactly(listenerError); + pyshell.end(function (err) { + should.not.exist(err); + done(); + }); + }); it('should not be invoked when mode is "binary"', function (done) { let pyshell = new PythonShell('stderrLogging.py', { stderrParser: 'binary',