Skip to content

fix(cli): errors to stderr, default fetch cap 20k chars - #118

Open
KGKallasmaa wants to merge 1 commit into
mainfrom
fix/errors-to-stderr-fetch-cap
Open

KGKallasmaa wants to merge 1 commit into
mainfrom
fix/errors-to-stderr-fetch-cap

Conversation

@KGKallasmaa

Copy link
Copy Markdown
Contributor

What

Two fixes from a user's feedback (daily CLI + MCP user, scripts parsing stdout):

  1. Errors go to stderr. On a failed search or fetch, stdout is now empty and the structured YAML error is written to stderr. It opens with an error: … line, so a script that reads stderr sees the failure at once. Exit codes are unchanged (3 auth, 4 rate limit, 5 credits, 6 target, 7 network, 8 invalid request, 9 server). The invalid KEENABLE_API_URL error follows the same rule. -p output is unchanged.
  2. fetch default cap is 20,000 characters. The CLI sends max_chars=20000 when --max-chars is not given, matching the MCP server. --max-chars still overrides it. Before, the API default of 50,000 could overflow an agent's context.

Breaking change

Scripts that parse the error YAML from stdout will break: read stderr instead. The one-line error: summary that used to be a separate stderr line is now the first line of the YAML.

Tests

On the devbox: cargo build --release, cargo test --release (157 unit, 13 api_url, 3 tls_proxy) and cargo fmt --check all pass. Live keyless runs of the built binary:

Case Exit stdout stderr starts with
bad API key 3 empty error: Authentication failed
404 fetch 6 empty error: Not found
30-request burst 4 (20 of 30) empty error: Rate limit exceeded
fetch, no flag 0 n/a n/a (20000 of 42880 chars, truncated: true)

The pytest e2e suite needs KEENABLE_API_KEY and was not run (no secrets on the devbox). Result.yaml() falls back to stderr on failure, so it works against old and new binaries (the nightly runs the latest release). New: test_fetch_default_cap_is_20k and a stdout-empty assertion in test_invalid_api_key.

Follow-ups

  • Docs PR in keenable-external-docs (error output stream, 20k default): not opened yet.
  • Release notes should flag the stdout → stderr change.

🤖 Generated with Claude Code

On a failed search or fetch, stdout is now empty and the structured YAML
error goes to stderr (it opens with an `error:` line). Exit codes are
unchanged. The invalid KEENABLE_API_URL error follows the same rule.

fetch now sends max_chars=20000 when --max-chars is not given, matching
the MCP server's default.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Send CLI errors to stderr and cap fetch output at 20,000 characters

🐞 Bug fix ✨ Enhancement 🧪 Tests 📝 Documentation 🕐 20-40 Minutes

Grey Divider

AI Description

• Send structured search and fetch errors to stderr, leaving stdout empty on failure.
• Default fetch requests to 20,000 characters while preserving --max-chars overrides.
• Update help, documentation, and tests for the new output contract and cap.
Diagram

graph TD
  CLI["CLI arguments"] --> Validation["URL validation"] --> Commands["Search or fetch"] --> Request["Fetch cap request"] --> Execution["Direct or daemon"] --> API["Keenable API"]
  Commands --> Execution
  Validation --> Stderr["Structured stderr"]
  Execution --> Errors["YAML error handler"] --> Stderr
Loading
High-Level Assessment

Keep the default in fetch request construction so direct and daemon execution receive the same explicit cap. Writing YAML directly to stderr at the existing error paths preserves the error fields and exit codes without changing successful output.

Files changed (11) +78 / -56

Bug fix (2) +12 / -10
api.rsSend invalid API URL YAML to stderr +4/-5

Send invalid API URL YAML to stderr

• Emits the structured invalid-KEENABLE_API_URL error and hint on stderr in YAML mode, without writing to stdout or changing its exit code.

src/api.rs

search.rsRedirect API errors and set the fetch default cap +8/-5

Redirect API errors and set the fetch default cap

• Writes structured search and fetch API errors to stderr instead of stdout. Fetch requests now carry a 20,000-character cap unless the caller supplies --max-chars.

src/commands/search.rs

Tests (5) +52 / -33
api_url.rsAssert URL and key-safety errors use stderr +15/-15

Assert URL and key-safety errors use stderr

• Changes integration assertions for invalid API URLs, rejected redirects, and stored-key protection to require empty stdout and inspect stderr.

tests/api_url.rs

conftest.pyRead error YAML from either binary output contract +11/-2

Read error YAML from either binary output contract

• Makes the e2e result parser fall back to stderr when a failed command has empty stdout. Adds a help-text probe so tests can distinguish newer binaries from older releases.

tests/e2e/conftest.py

test_fetch.pyTest the default 20,000-character fetch cap +11/-0

Test the default 20,000-character fetch cap

• Adds a live fetch check for capped content and the truncation flag, skipping the assertion when the returned page is shorter than the cap.

tests/e2e/test_fetch.py

test_search.pyCheck failed authentication leaves stdout empty +6/-3

Check failed authentication leaves stdout empty

• Extends the invalid-key test to assert stderr starts with the error YAML and stdout is empty for binaries advertising the new behavior.

tests/e2e/test_search.py

tls_proxy.rsAssert TLS failures report through stderr +9/-13

Assert TLS failures report through stderr

• Updates proxy and certificate-failure tests to require empty stdout and check error messages and hints on stderr.

tests/tls_proxy.rs

Documentation (4) +14 / -13
CLAUDE.mdDocument stderr-only structured errors +3/-3

Document stderr-only structured errors

• Revises the CLI guidance for API and invalid-base-URL errors to specify YAML on stderr and empty stdout.

CLAUDE.md

README.mdDocument the fetch cap and scripting error contract +6/-5

Document the fetch cap and scripting error contract

• Changes the documented default content cap to 20,000 characters and tells scripts to read failed search and fetch YAML from stderr.

README.md

daemon.rsClarify the fetch cap request field +1/-1

Clarify the fetch cap request field

• Updates the DaemonRequest field documentation to reflect the CLI's new 20,000-character default.

src/daemon.rs

main.rsAlign CLI help with stderr errors and the new cap +4/-4

Align CLI help with stderr errors and the new cap

• Updates search and fetch help text to describe empty stdout on failure and structured YAML on stderr. Fetch help now states the 20,000-character default.

src/main.rs

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (4) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Verbose failures break error YAML 🐞 Bug ≡ Correctness
Description
handle_api_error prints a human-readable Error chain: before the newly redirected YAML when
verbose mode is enabled and the error has multiple causes. A search -v or fetch -v transport
failure then produces stderr that neither starts with error: nor parses as one YAML document.
Code

src/commands/search.rs[R523-524]

+        let yaml = serde_yaml::to_string(&val).unwrap_or_default();
+        eprint!("{}", yaml);
Evidence
The YAML branch emits ui::info and ui::sub_info before the changed eprint!; transport errors
preserve a chain that can contain multiple entries. Previously those diagnostics were on stderr
while the YAML remained on stdout.

src/commands/search.rs[501-524]
src/api.rs[219-232]
src/main.rs[355-357]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Verbose transport errors print a human-readable cause chain before the YAML now written to stderr, breaking stderr parsing.
## Fix Focus Areas
- src/commands/search.rs[501-524]
## Recommended Fix
Do not emit separate verbose chain lines in YAML mode; retain the cause information in the structured YAML fields. Keep human-readable chain output in pretty mode.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Dismiss ↗ | View ↗


2. Nightly tests fail before the next release 🐞 Bug ☼ Reliability
Description
test_fetch_default_cap_is_20k unconditionally requires behavior introduced in this PR, but the
nightly workflow runs the checked-out tests against the latest published binary. Until a binary with
the new default is released, a fetched page longer than 20,000 characters is subject to the older
50,000-character API default and fails this assertion.
Code

tests/e2e/test_fetch.py[R77-80]

+    if data.get("total_chars", 0) <= 20000:
+        pytest.skip("page shorter than the cap")
+    assert len(data["content"]) <= 20000
+    assert data["truncated"] is True
Evidence
The workflow installs releases/latest unless a version is supplied, then runs repository tests
against that installed binary. The new test has no version guard, while this PR changes an omitted
max_chars from the former API default to an explicit 20,000.

.github/workflows/e2e.yml[16-17]
.github/workflows/e2e.yml[70-83]
.github/workflows/e2e.yml[126-137]
tests/e2e/test_fetch.py[72-80]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The scheduled E2E suite runs current tests against the latest release, which does not yet have the new fetch default.
## Fix Focus Areas
- tests/e2e/test_fetch.py[72-80]
- tests/e2e/conftest.py[74-77]
## Recommended Fix
Detect whether the installed binary advertises the 20,000-character default and skip this particular default-cap assertion for older binaries; still run it against builds that advertise the new default.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Dismiss ↗ | View ↗


3. Older truncation responses fail the new test 🐞 Bug ≡ Correctness
Description
test_fetch_default_cap_is_20k requires the entire returned content to be at most 20,000
characters, although the older supported API appends a truncation notice beyond the requested cap.
When that response format is returned for a long page, the test fails despite the CLI sending and
the API applying the new default.
Code

tests/e2e/test_fetch.py[R79-80]

+    assert len(data["content"]) <= 20000
+    assert data["truncated"] is True
Evidence
The adjacent explicit-cap test documents and accepts the older appended notice; the CLI recognizes
that notice and marks the response truncated without removing it.

tests/e2e/test_fetch.py[51-80]
src/commands/search.rs[225-252]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new default-cap test rejects the older truncation response format already accepted by the explicit-cap test.
## Fix Focus Areas
- tests/e2e/test_fetch.py[51-80]
## Recommended Fix
Apply the neighboring test's legacy-notice handling to the 20,000-character assertion. For responses without that notice, retain the content-length and truncated-field checks.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Dismiss ↗ | View ↗


View medium (1)
4. Missing response data skips the cap test 🐞 Bug ☼ Reliability
Description
test_fetch_default_cap_is_20k uses zero as the fallback for a missing total_chars and treats
that value as evidence that the page is short. If the API omits that field for a long response, the
suite skips its only assertion of the new default rather than detecting the missing data.
Code

tests/e2e/test_fetch.py[R77-78]

+    if data.get("total_chars", 0) <= 20000:
+        pytest.skip("page shorter than the cap")
Evidence
The new .get(..., 0) branch skips on an absent field; the existing explicit-cap test instead
requires and checks total_chars.

tests/e2e/test_fetch.py[60-80]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A missing `total_chars` is mistaken for a short page, so the new default-cap test can silently skip.
## Fix Focus Areas
- tests/e2e/test_fetch.py[76-80]
## Recommended Fix
Assert that `total_chars` exists and is numeric before using it to decide whether the response page is too short to test the cap.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Dismiss ↗ | View ↗


Grey Divider

Context sources
Review mode: Auto: ⚖️ Balanced: Public CLI contract changes across multiple error and fetch paths.

Grey Divider

Tip of the day
💡 Did you know, you can start a comment with 'qodo' or '@qodo' to chat about any finding

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread src/commands/search.rs
Comment on lines +523 to +524
let yaml = serde_yaml::to_string(&val).unwrap_or_default();
eprint!("{}", yaml);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

1. Verbose failures break error yaml 🐞 Bug ≡ Correctness

handle_api_error prints a human-readable Error chain: before the newly redirected YAML when
verbose mode is enabled and the error has multiple causes. A search -v or fetch -v transport
failure then produces stderr that neither starts with error: nor parses as one YAML document.
Agent Prompt
## Issue description
Verbose transport errors print a human-readable cause chain before the YAML now written to stderr, breaking stderr parsing.
## Fix Focus Areas
- src/commands/search.rs[501-524]
## Recommended Fix
Do not emit separate verbose chain lines in YAML mode; retain the cause information in the structured YAML fields. Keep human-readable chain output in pretty mode.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Dismiss ↗ | View ↗

Comment thread tests/e2e/test_fetch.py
Comment on lines +77 to +80
if data.get("total_chars", 0) <= 20000:
pytest.skip("page shorter than the cap")
assert len(data["content"]) <= 20000
assert data["truncated"] is True

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

2. Nightly tests fail before the next release 🐞 Bug ☼ Reliability

test_fetch_default_cap_is_20k unconditionally requires behavior introduced in this PR, but the
nightly workflow runs the checked-out tests against the latest published binary. Until a binary with
the new default is released, a fetched page longer than 20,000 characters is subject to the older
50,000-character API default and fails this assertion.
Agent Prompt
## Issue description
The scheduled E2E suite runs current tests against the latest release, which does not yet have the new fetch default.
## Fix Focus Areas
- tests/e2e/test_fetch.py[72-80]
- tests/e2e/conftest.py[74-77]
## Recommended Fix
Detect whether the installed binary advertises the 20,000-character default and skip this particular default-cap assertion for older binaries; still run it against builds that advertise the new default.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Dismiss ↗ | View ↗

Comment thread tests/e2e/test_fetch.py
Comment on lines +79 to +80
assert len(data["content"]) <= 20000
assert data["truncated"] is True

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

3. Older truncation responses fail the new test 🐞 Bug ≡ Correctness

test_fetch_default_cap_is_20k requires the entire returned content to be at most 20,000
characters, although the older supported API appends a truncation notice beyond the requested cap.
When that response format is returned for a long page, the test fails despite the CLI sending and
the API applying the new default.
Agent Prompt
## Issue description
The new default-cap test rejects the older truncation response format already accepted by the explicit-cap test.
## Fix Focus Areas
- tests/e2e/test_fetch.py[51-80]
## Recommended Fix
Apply the neighboring test's legacy-notice handling to the 20,000-character assertion. For responses without that notice, retain the content-length and truncated-field checks.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Dismiss ↗ | View ↗

Comment thread tests/e2e/test_fetch.py
Comment on lines +77 to +78
if data.get("total_chars", 0) <= 20000:
pytest.skip("page shorter than the cap")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

4. Missing response data skips the cap test 🐞 Bug ☼ Reliability

test_fetch_default_cap_is_20k uses zero as the fallback for a missing total_chars and treats
that value as evidence that the page is short. If the API omits that field for a long response, the
suite skips its only assertion of the new default rather than detecting the missing data.
Agent Prompt
## Issue description
A missing `total_chars` is mistaken for a short page, so the new default-cap test can silently skip.
## Fix Focus Areas
- tests/e2e/test_fetch.py[76-80]
## Recommended Fix
Assert that `total_chars` exists and is numeric before using it to decide whether the response page is too short to test the cap.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Dismiss ↗ | View ↗

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Auto-approved: Qodo review completed — 🐞 Bugs (4),📘 Rule violations (0) 📜 Skill insights (0). Findings are advisory per team policy; verdict source: #118 (comment) [auto-approve.yml]

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