Skip to content

Report java failures from the Node.js wrapper instead of returning empty output - #19

Merged
marevol merged 1 commit into
fix/nodejs-jar-selectionfrom
fix/nodejs-report-java-failures
Sep 5, 2026
Merged

marevol merged 1 commit into
fix/nodejs-jar-selectionfrom
fix/nodejs-report-java-failures

Conversation

@marevol

@marevol marevol commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Stacked on #18. Base branch is fix/nodejs-jar-selection; review that one
first. #18 fixes which jar the wrapper runs - this one fixes why running the
wrong one looked like success.

The defect

Three ways the wrapper could not report a failure.

1. Only the string [ERROR] counted as an error.

child.on('exit', function() {
    var err = null;
    if (errBuffer.indexOf('[ERROR]') > -1) { err = errBuffer; }
    callback(err, buffer, errBuffer);
});

Anything that killed the JVM before the compressor could print that marker came
back as err === null with '' for the compressed output. That is how the
NoClassDefFoundError in #18 became a silently empty minified file rather than
a build failure. The exit code was right there in the handler and unused.

2. java missing from PATH never called back at all.

spawn emits 'error' and never emits 'exit', and nothing listened for it,
so the ENOENT surfaced as an uncaught exception in whatever code happened to be
running - and the caller's callback was never invoked.

3. A read error was discarded.

getString(str, function(err, str, options) {
    compressString(str, options, callback);   // err dropped
}, options);

A path that exists but cannot be read (a directory, a permissions failure) left
str undefined, and child.stdin.write(undefined) threw
ERR_INVALID_ARG_TYPE from inside the wrapper.

Change

  • A non-zero exit is an error whatever stderr contains.
  • 'error' on the child, and a broken pipe on its stdin, are both handled; a
    single settle() guarantees the caller is called back exactly once.
  • The read error is passed to the caller.

The existing contract is preserved: when the compressor itself reports a
problem, err is still the stderr text. Only the cases that previously reported
nothing produce an Error object, and only when stderr is empty.

Verification

The three new tests fail on the unfixed wrapper, each for its own reason:

not ok 5 - reports a non-zero exit even when stderr carries no [ERROR] marker
  error: 'a java process that exits non-zero must be reported as an error'
not ok 6 - reports a spawn failure instead of hanging
  error: 'spawn java ENOENT'   code: 'ENOENT'      <- uncaught, crashed the runner
not ok 7 - reports a file that cannot be read
  error: 'The "chunk" argument must be of type string ... Received undefined'

and pass after the change (# pass 7 # fail 0). Replaying the original
incident - wrapper pointed at the pre-shade jar - now returns the real cause
instead of an empty string:

before:  err: null                                              out: ""
after :  err: NoClassDefFoundError: org/kohsuke/args4j/...      out: ""

nodejs/cli.js is unaffected; the Java suite is unaffected (819 tests, 0 failures).

…pty output

Stacked on "Run the shaded jar from the Node.js wrapper".

That change fixed which jar the wrapper runs. This one fixes why running the
wrong one looked like success. Three ways the wrapper could not report a
failure:

  1. `err` was derived solely from the substring '[ERROR]' appearing in stderr.
     Anything that killed the JVM before the compressor could print that marker
     came back as err === null with '' as the compressed output - which is how a
     NoClassDefFoundError became a silently empty minified file. A non-zero exit
     is now an error whatever stderr contains.

  2. spawn emits 'error' and never emits 'exit' when java is not on PATH, and
     nothing listened for it. The callback was never invoked and the ENOENT was
     thrown as an uncaught exception. 'error' on the child and a broken pipe on
     its stdin are both handled now, and a single settle() guarantees the caller
     is called back exactly once.

  3. compress() took the error from fs.readFile and dropped it, passing
     undefined to child.stdin.write - ERR_INVALID_ARG_TYPE thrown from inside
     the wrapper. It is passed to the caller instead.

The existing contract is preserved: when the compressor itself reports a
problem, err is still the stderr text. Only the cases that previously reported
nothing produce an Error object, and only when stderr is empty.

Verified: the three new tests fail on the unfixed wrapper for their own
reasons - err null despite a non-zero exit, an uncaught 'spawn java ENOENT',
and ERR_INVALID_ARG_TYPE on an unreadable path - and pass after the change.
Replaying the original incident (wrapper pointed at the pre-shade jar) now
returns the NoClassDefFoundError instead of an empty string. nodejs/cli.js is
unaffected.
@marevol
marevol force-pushed the fix/nodejs-report-java-failures branch from 19b561d to 46b6e92 Compare September 5, 2026 04:37
@marevol marevol added this to the 2.4.11 milestone Sep 5, 2026
@marevol marevol self-assigned this Sep 5, 2026
@marevol
marevol merged commit 9718b33 into main Sep 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant