Skip to content

Add --passstderr flag to forward STDERR through WebSocket - #459

Closed
Formatted wants to merge 1 commit into
joewalnes:masterfrom
Formatted:passstderr-feature
Closed

Add --passstderr flag to forward STDERR through WebSocket#459
Formatted wants to merge 1 commit into
joewalnes:masterfrom
Formatted:passstderr-feature

Conversation

@Formatted

Copy link
Copy Markdown
Contributor

Summary

Adds a --passstderr CLI flag that forwards STDERR output from the wrapped process to WebSocket clients as tagged JSON messages.

Addresses: #403 (Feature Request: STDERR redirected through websockets, open since 2021)

Problem

Currently, STDERR output from subprocesses is silently swallowed — it's only logged on the server side and never reaches the WebSocket client. This makes debugging scripts nearly impossible, as warn/die in Perl, Python tracebacks, and other error output is invisible to clients.

Solution

When --passstderr is enabled:

  • STDERR lines are forwarded as JSON: {"stream":"stderr","data":"error message"}
  • STDOUT lines are also tagged for consistency: {"stream":"stdout","data":"output line"}
  • The output channel closes only after both STDOUT and STDERR complete, ensuring no messages are lost

When --passstderr is not set (default), behavior is unchanged — STDERR is only logged server-side.

Example

websocketd --port=8080 --passstderr ./myscript.sh

Client receives:

{"stream":"stdout","data":"normal output"}
{"stream":"stderr","data":"warning: something went wrong"}

Client-side usage

ws.onmessage = function(event) {
  var msg = JSON.parse(event.data);
  if (msg.stream === 'stderr') {
    console.error(msg.data);
  } else {
    console.log(msg.data);
  }
};

Changes

  • libwebsocketd/config.go — Added PassStderr field to Config
  • libwebsocketd/process_endpoint.go — Added passStderr parameter to NewProcessEndpoint, new readStdoutTagged/readStderrTagged methods with sync.WaitGroup, and tagMessage helper with proper JSON escaping
  • libwebsocketd/handler.go — Wire PassStderr config through to NewProcessEndpoint
  • config.go — Added --passstderr CLI flag
  • help.go — Updated help text
  • README.md — Mentioned new flag in features list
  • CHANGES — Added changelog entry
  • libwebsocketd/process_endpoint_stderr_test.go — 3 unit tests covering the feature

Testing

  • All 217 existing tests pass
  • 3 new tests added:
    1. STDERR forwarded as JSON when passStderr enabled
    2. STDERR not forwarded when passStderr disabled
    3. Output channel closes after both STDOUT and STDERR complete
  • Built and vetted on Go 1.21

…alnes#403)

When --passstderr is enabled, STDERR output from the wrapped process
is forwarded to WebSocket clients as tagged JSON messages:
  {"stream":"stderr","data":"error message"}

STDOUT messages are also tagged for consistency:
  {"stream":"stdout","data":"output line"}

This addresses the long-standing issue (joewalnes#403) where STDERR was
silently swallowed, making it impossible for clients to see error
output from their scripts (e.g., Perl warn, Python tracebacks).

The output channel now closes only after both STDOUT and STDERR
goroutines complete, ensuring no messages are lost.

- Added PassStderr field to libwebsocketd.Config
- Added passStderr parameter to NewProcessEndpoint
- Added readStdoutTagged/readStderrTagged methods with sync.WaitGroup
- Added tagMessage helper with proper JSON string escaping
- Added 3 unit tests covering the new behavior
- Updated --help text and README
joewalnes pushed a commit that referenced this pull request Jul 10, 2026
Rebase and rework of #459 (by @Formatted) onto current master.

Forwards STDERR to WebSocket clients as tagged JSON, alongside tagged
STDOUT, so a client can tell the two apart:
  {"stream":"stdout","data":"..."}
  {"stream":"stderr","data":"..."}
STDERR is still logged server-side either way, same as without the
flag. Addresses #403 (open since 2021).

Changes from the original PR:
- The tagged stdout/stderr readers now integrate with the done-channel
  leak fix from the earlier goroutine-leak PR: each select{}s on the
  output send against Terminate's done signal, same as the plain text
  and binary readers, instead of blocking unconditionally. Verified
  by temporarily reverting just that part and watching the new
  regression test fail (3 leaked goroutines), then restoring it.
- --binary and --passstderr are now mutually exclusive, rejected at
  startup with a clear error. The original PR silently dropped
  --binary whenever --passstderr was set (StartReading branched on
  passStderr before bin), which would corrupt binary output instead
  of erroring - tagging arbitrary binary chunks as JSON string data
  isn't implemented, so refusing the combination is safer than a
  partial implementation.
- JSON encoding now goes through encoding/json (a small taggedMessage
  struct) instead of a hand-rolled escaper, so it can't emit invalid
  JSON for control characters or non-UTF8 bytes the original escaper
  didn't handle.
- Added a --binary/--passstderr validation unit test, a goroutine-leak
  regression test mirroring the process-endpoint one, an integration
  test asserting the tagged JSON over a real WebSocket connection (and
  that STDERR still reaches the server log), and a QA plan entry.

Co-Authored-By: Formatted <[email protected]>
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01M882UWfvyaq5KGvaV37idr
joewalnes pushed a commit that referenced this pull request Jul 10, 2026
Rebase and rework of #459 (by @Formatted) onto current master.

Forwards STDERR to WebSocket clients as tagged JSON, alongside tagged
STDOUT, so a client can tell the two apart:
  {"stream":"stdout","data":"..."}
  {"stream":"stderr","data":"..."}
STDERR is still logged server-side either way, same as without the
flag. Addresses #403 (open since 2021).

Changes from the original PR:
- The tagged stdout/stderr readers now integrate with the done-channel
  leak fix from the earlier goroutine-leak PR: each select{}s on the
  output send against Terminate's done signal, same as the plain text
  and binary readers, instead of blocking unconditionally. Verified
  by temporarily reverting just that part and watching the new
  regression test fail (3 leaked goroutines), then restoring it.
- --binary and --passstderr are now mutually exclusive, rejected at
  startup with a clear error. The original PR silently dropped
  --binary whenever --passstderr was set (StartReading branched on
  passStderr before bin), which would corrupt binary output instead
  of erroring - tagging arbitrary binary chunks as JSON string data
  isn't implemented, so refusing the combination is safer than a
  partial implementation.
- JSON encoding now goes through encoding/json (a small taggedMessage
  struct) instead of a hand-rolled escaper, so it can't emit invalid
  JSON for control characters or non-UTF8 bytes the original escaper
  didn't handle.
- Added a --binary/--passstderr validation unit test, a goroutine-leak
  regression test mirroring the process-endpoint one, an integration
  test asserting the tagged JSON over a real WebSocket connection (and
  that STDERR still reaches the server log), and a QA plan entry.

Co-Authored-By: Formatted <[email protected]>
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01M882UWfvyaq5KGvaV37idr

Copy link
Copy Markdown
Owner

Thanks for this — a real gap (#403 has been open since 2021). Reworked and rebased as #464: integrated with the goroutine-leak fix from an earlier PR (the tagged readers here predate it and would have reintroduced the same leak class), made --binary/--passstderr mutually exclusive with a clear error instead of silently dropping --binary, switched the JSON encoding to encoding/json, and added test coverage. Closing this in favor of that one — you're credited as a co-author on the commit.


Generated by Claude Code

@joewalnes joewalnes closed this Jul 10, 2026
joewalnes added a commit that referenced this pull request Jul 10, 2026
Rebase and rework of #459 (by @Formatted) onto current master.

Forwards STDERR to WebSocket clients as tagged JSON, alongside tagged
STDOUT, so a client can tell the two apart:
  {"stream":"stdout","data":"..."}
  {"stream":"stderr","data":"..."}
STDERR is still logged server-side either way, same as without the
flag. Addresses #403 (open since 2021).

Changes from the original PR:
- The tagged stdout/stderr readers now integrate with the done-channel
  leak fix from the earlier goroutine-leak PR: each select{}s on the
  output send against Terminate's done signal, same as the plain text
  and binary readers, instead of blocking unconditionally. Verified
  by temporarily reverting just that part and watching the new
  regression test fail (3 leaked goroutines), then restoring it.
- --binary and --passstderr are now mutually exclusive, rejected at
  startup with a clear error. The original PR silently dropped
  --binary whenever --passstderr was set (StartReading branched on
  passStderr before bin), which would corrupt binary output instead
  of erroring - tagging arbitrary binary chunks as JSON string data
  isn't implemented, so refusing the combination is safer than a
  partial implementation.
- JSON encoding now goes through encoding/json (a small taggedMessage
  struct) instead of a hand-rolled escaper, so it can't emit invalid
  JSON for control characters or non-UTF8 bytes the original escaper
  didn't handle.
- Added a --binary/--passstderr validation unit test, a goroutine-leak
  regression test mirroring the process-endpoint one, an integration
  test asserting the tagged JSON over a real WebSocket connection (and
  that STDERR still reaches the server log), and a QA plan entry.



Claude-Session: https://claude.ai/code/session_01M882UWfvyaq5KGvaV37idr

Co-authored-by: Claude <[email protected]>
Co-authored-by: Formatted <[email protected]>
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.

2 participants