fix(cli): read stdin lazily so open pipes don't hang the CLI - #1330
Open
l2ysho wants to merge 2 commits into
Open
fix(cli): read stdin lazily so open pipes don't hang the CLI#1330l2ysho wants to merge 2 commits into
l2ysho wants to merge 2 commits into
Conversation
l2ysho
marked this pull request as ready for review
August 14, 2026 11:02
Every command paid for stdin: `_shared.ts` did `await readStdin()` at module scope, so `apify --version` blocked on a stream it never reads. With a named pipe (no wait deadline, unlike the socket a spawned child gets) the read never ended and the command never even ran. Read stdin only when a command asks for it — a `-` arg/flag, `apify run` without `--input`, `push-data`/`push-items` without an item. Result is memoized, since stdin can only be drained once. Confirmations previously keyed off `hasData`, which the eager read flipped to false once stdin was drained. Without that read the flag stays true, so `apify <confirm-cmd> < /dev/null` would try to prompt on a non-TTY; gate on `isTTY` instead. Commands that genuinely consume stdin still wait for the writer to close, same as `cat`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
l2ysho
force-pushed
the
claude/apify-cli-1206-verify-37b57c
branch
from
September 3, 2026 12:25
29e4187 to
2b8ebae
Compare
`apify run` without `--input` falls back to stdin, and waited there for an end that a named pipe never reaches. A pipe the process merely inherited hung the command forever, and so did a writer that sent data and kept the pipe open. `actors call` and `actors start` share the path. Reads that nobody asked for now stop at the first quiet gap: each chunk restarts a 2s clock, and when stdin goes silent the CLI uses what arrived. An explicit `-` is unchanged and still waits for the writer to close. The cost is a writer slower than ~2.5s to its first byte, whose input is now ignored. Truncation mid-stream stays loud, as the JSON fails to parse. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Note
TL;DR — The CLI read stdin at startup for every command, so a pipe that stays open hung it forever. Now it only reads stdin when a command wants it, and a read nobody asked for gives up when the pipe goes quiet.
Closes #1206.
The startup read
_shared.tsread stdin at module scope (export const cachedStdinInput = await readStdin()), so every command blocked on stdin whether or not it uses it. #1294 fixed the socket case (a spawned child'sstdio: 'pipe'), but a named pipe has no wait deadline — the read never ended and the command never even ran, so 1.8.0 still hangs with no output at all:Read stdin only where it's asked for: a
-arg/flag,apify runwithout--input,push-data/push-itemswithout an item arg.readStdin()memoizes, since stdin can only be drained once.Confirmations keyed off
hasData, which the eager read flipped to false after draining. Without that read it stays true, soapify <confirm-cmd> < /dev/nullwould try to prompt on a non-TTY — gated onisTTYinstead.The implicit read
That left one path still hanging.
apify runwithout--inputfalls back to stdin, and waited there for an end a named pipe never reaches.actors callandactors startshare it. Two ways to hang:A read the user never asked for now stops at the first quiet gap: each chunk restarts a 2s clock, and when stdin goes silent the CLI uses what arrived. An explicit
-is unchanged — it waits for the writer to close, the waycatdoes.Trade-off: a writer slower than ~2.5s to its first byte is now ignored (
curl slow-endpoint | apify run). Truncation mid-stream stays loud, since the JSON fails to parse. 2s is a judgement call, not a derived number.Verification
Measured against a build of master and a build of this branch.
--version--version--versionstdio: 'pipe'(#1294's case)--versionrunin a projectrunin a projectrunin a projectrunin a projectThree new e2e cases, all skipped on Windows (no
mkfifo). Each fails on the pre-fix build at the 15s deadline and passes after:pnpm run test:localgreen (540 passed).pnpm run test:e2e:localgreen (44 passed). No dependency or install-size change.🤖 Generated with Claude Code