Skip to content

fix: flush stdout before process.exit() to prevent pipe truncation#17

Merged
jancurn merged 1 commit intoapify:mainfrom
vystrcild:fix/flush-stdout-before-exit
Mar 3, 2026
Merged

fix: flush stdout before process.exit() to prevent pipe truncation#17
jancurn merged 1 commit intoapify:mainfrom
vystrcild:fix/flush-stdout-before-exit

Conversation

@vystrcild
Copy link
Copy Markdown
Contributor

Summary

When mcpc JSON output exceeds 64KB and stdout is piped (e.g. mcpc ... --json | jq), the output is silently truncated at exactly 65,536 bytes, producing broken JSON that downstream parsers can't read.

Root cause

src/cli/index.ts line 232 calls process.exit(0) after command execution. When stdout is a pipe, Node.js uses async I/O — console.log() buffers data internally and flushes it asynchronously. process.exit() terminates the process before the buffer is fully written to the pipe, discarding any data beyond the 64KB kernel pipe buffer.

When stdout is a file or TTY, Node.js uses synchronous writeSync(), so all data is written before process.exit() runs — which is why mcpc ... --json > file.json always works.

Reproduction

# Pipe: truncated at exactly 64KB
mcpc @apify tools-call get-actor-output datasetId:="<large>" limit:=25 --json 2>/dev/null | wc -c
# → 65536

# File redirect: complete output
mcpc @apify tools-call get-actor-output datasetId:="<large>" limit:=25 --json > /tmp/out.json 2>/dev/null && wc -c /tmp/out.json
# → 118166

# jq fails with broken JSON
mcpc ... --json | jq '.structuredContent.items'
# → jq: parse error: Unfinished string at EOF at line 194, column 22

Fix

Wait for stdout to fully drain before calling process.exit(0). The exit itself is still needed (MCP SDK's StdioClientTransport keeps the event loop alive), but now it only fires after all buffered data has been flushed.

Verified

# After fix — pipe returns complete output
mcpc ... --json 2>/dev/null | wc -c
# → 118166 ✓

# jq parses correctly
mcpc ... --json | jq '.structuredContent | {totalItemCount, count: (.items | length)}'
# → {"totalItemCount": 25, "count": 25} ✓

# File redirect still works, process exits cleanly (no hang)

Test plan

  • Pipe output >64KB through jq — should parse without errors
  • File redirect — should produce identical byte count as pipe
  • Process should exit cleanly (no hanging from MCP SDK handles)
  • Small outputs (<64KB) should still work correctly through pipes

🤖 Generated with Claude Code

When mcpc output exceeds 64KB and stdout is piped (e.g. `mcpc ... --json | jq`),
the output was silently truncated at exactly 65,536 bytes (the kernel pipe buffer
size), producing broken JSON.

Root cause: when stdout is a pipe, Node.js uses async I/O. `process.exit(0)`
terminates the process before buffered data is fully written to the pipe. When
stdout is a file or TTY, Node.js uses sync I/O, so data is written before exit.

The fix waits for stdout to drain before calling process.exit(0).

Reproduction:
  mcpc @apify tools-call get-actor-output datasetId:="..." --json | wc -c
  # Before: 65536 (truncated)  After: 118166 (complete)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@jancurn jancurn merged commit 1e35f2f into apify:main Mar 3, 2026
4 of 5 checks passed
MQ37 added a commit that referenced this pull request Mar 3, 2026
commit 1e35f2f
Author: Dušan Vystrčil <vystrcil.dusan@gmail.com>
Date:   Tue Mar 3 10:25:50 2026 +0100

    fix: flush stdout before process.exit() to prevent pipe truncation (#17)

    When mcpc output exceeds 64KB and stdout is piped (e.g. `mcpc ... --json | jq`),
    the output was silently truncated at exactly 65,536 bytes (the kernel pipe buffer
    size), producing broken JSON.

    Root cause: when stdout is a pipe, Node.js uses async I/O. `process.exit(0)`
    terminates the process before buffered data is fully written to the pipe. When
    stdout is a file or TTY, Node.js uses sync I/O, so data is written before exit.

    The fix waits for stdout to drain before calling process.exit(0).

    Reproduction:
      mcpc @apify tools-call get-actor-output datasetId:="..." --json | wc -c

    Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

commit c7ea6f2
Author: Jan Curn <jan.curn@gmail.com>
Date:   Mon Mar 2 13:15:22 2026 +0100

    Updated CHANGELOG.md, improved release process

commit 3861f51
Author: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Date:   Mon Mar 2 11:19:49 2026 +0100

    Add renovate.json (#13)

    Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

commit ba193b8
Author: Jan Curn <jan.curn@gmail.com>
Date:   Sun Mar 1 23:36:16 2026 +0100

    v0.1.10

commit 8400ca3
Author: Jan Curn <jan.curn@gmail.com>
Date:   Sun Mar 1 23:21:57 2026 +0100

    Added support for `HTTPS_PROXY` / `HTTP_PROXY` / `NO_PROXY` env vars (#10)

    * Added support for `HTTPS_PROXY` / `https_proxy` / `HTTP_PROXY` / `http_proxy` env vars

    * Improvements

    * Improvements

    * Fixes

    * Fixes

    * Nits
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.

3 participants