Skip to content

cli: an unreadable input escapes as a raw traceback with exit 1, in four places that promise an exit-2 document #63

Description

@Shashankss1205

What happens

Four code paths read a file with a call whose exception type the surrounding handler does not catch. The result is a Python traceback on stderr and exit 1, where the documented contract is exit 2 with {"ok": false, ...} on stdout — "an unreadable trace" is the literal example given for EXIT_UNAVAILABLE in cli/output.py:26-27.

Repro (all verified)

$ mkdir adir
$ grapharc trace   adir   --json   → exit=1  stdout=0B  stderr=954B   IsADirectoryError traceback
$ grapharc metrics adir x --json   → exit=1  stdout=0B  stderr=1090B  IsADirectoryError traceback
$ grapharc viz     adir x --json   → exit=1  stdout=0B  stderr=1385B  IsADirectoryError traceback
$ grapharc replay  adir x --json   → exit=1  stdout=138B stderr=0B    clean document   <- correct

$ chmod 000 t.jsonl; grapharc trace t.jsonl --json  → PermissionError traceback

$ printf '\xff\xfe\x00bin' > bin.json
$ grapharc run bin.json --json     → exit=1  stdout=0B  UnicodeDecodeError traceback

$ printf '\xff\xfe' > grapharc.toml
$ grapharc demo stage0 --json      → exit=1  stdout=0B  UnicodeDecodeError traceback

Root causes

  1. _existing_trace (cli/main.py:239-248) only tests path.exists(), and the handlers at main.py:495 / :531 / :555 catch only TraceReadError, so OSError (directory, permission, I/O) escapes. replay/diff handle this correctly — the family is inconsistent with itself, which is the clearest sign it is an oversight.
  2. cli/graphrun.py:73path.read_text(encoding="utf-8") sits outside the except (JSONDecodeError, TOMLDecodeError) on line 78, and UnicodeDecodeError is neither.
  3. cli/config.py:164-166 — same shape: read_text inside the tomllib.loads(...) argument, but UnicodeDecodeError is not in except (OSError, TOMLDecodeError).

Why it matters

The exit-code contract is described in the README as part of the interface, and #37 fixed exactly this class for malformed trace lines one release ago — these are the remaining doors. The config case is the nastiest: grapharc.toml is picked up implicitly from the working directory, so a stray binary file there breaks demo, run and plan with a traceback rather than a report naming the file.

What to consider

  • Widen _existing_trace to attempt the read (or path.is_file() plus catching OSError) and route through fail(...) with EXIT_UNAVAILABLE, exactly as replay already does.
  • Add UnicodeDecodeError to the two except tuples, with a message naming the file and the fact that it is not UTF-8.
  • One parametrized test per path (directory, unreadable, non-UTF-8) asserting exit 2, one document on stdout, empty stderr — the same shape test_cli.py already uses for the malformed-line cases.

Related

Not the same bug, but adjacent and worth deciding together: argparse usage errors in --json mode (grapharc trace --json, demo stage99 --json) print text to stderr rather than a document. Exit code 2 is correct there; only the document half is missing. Fixing it needs a custom ArgumentParser.error, so it may deserve its own issue.

Acceptance criteria

Each of the seven commands above exits 2 with one {"ok": false, ...} document on stdout and empty stderr; text mode prints error: ... on stderr with empty stdout; no traceback reaches a user in any case.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workinghelp wantedExtra attention is needed

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions