Skip to content

fix: improve raw API diagnostics for invalid or empty JSON responses#257

Open
vuth-dogo wants to merge 2 commits intolarksuite:mainfrom
vuth-dogo:fix/issue-215-api-json-response-diagnostics
Open

fix: improve raw API diagnostics for invalid or empty JSON responses#257
vuth-dogo wants to merge 2 commits intolarksuite:mainfrom
vuth-dogo:fix/issue-215-api-json-response-diagnostics

Conversation

@vuth-dogo
Copy link
Copy Markdown

@vuth-dogo vuth-dogo commented Apr 3, 2026

Summary

Improves lark-cli api UX when an endpoint returns an empty body or non-JSON content that still fails JSON parsing: failures are reported as api_error with a clear message and a hint to use --output for file/binary-style responses (see #215).

Changes

  • Add internal/client/api_errors.go with WrapDoAPIError and WrapJSONResponseParseError to classify JSON decode issues vs generic network errors.
  • Route cmd/api DoAPI errors and HandleResponse JSON parse errors through the new helpers.
  • Add regression tests in cmd/api and internal/client.

Test Plan

  • go test ./cmd/api ./internal/client -run 'TestApiCmd_InvalidJSONResponse_ShowsDiagnostic|TestHandleResponse_EmptyJSONBody_ShowsDiagnostic'
  • go mod tidy (no go.mod/go.sum changes)
  • golangci-lint --new-from-rev=origin/main (0 issues)
  • GOTOOLCHAIN=go1.23.8 make unit-testpass (darwin/arm64); confirms full suite is green when pinned to Go 1.23.8 as in coordinator validation.
  • Note: default go1.26.0 on the same host still hits SIGSEGV in the race-enabled unit suite for some packages; CI / supported Go version should be treated as the canonical gate for Go 1.26+ behavior.

Related Issues

Summary by CodeRabbit

  • Bug Fixes
    • Improved error handling and diagnostics for API response failures, including better detection and messaging for invalid or empty JSON responses.
    • Enhanced error messages now include actionable hints, such as suggestions to use the --output flag for troubleshooting.
    • Refined distinction between network and API errors for clearer error classification.

Wrap JSON decode failures from DoAPI and HandleResponse in api_error
exit details with a hint to use --output when the endpoint may return
non-JSON payloads.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Made-with: Cursor
@github-actions github-actions bot added the size/L Large or sensitive change across domains or core paths label Apr 3, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 3, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 379e9439-9e20-411b-801b-0c10a329d85b

📥 Commits

Reviewing files that changed from the base of the PR and between 96c5d8d and a40a628.

📒 Files selected for processing (5)
  • cmd/api/api_test.go
  • internal/client/api_errors.go
  • internal/client/api_errors_test.go
  • internal/client/response.go
  • internal/client/response_test.go
🚧 Files skipped from review as they are similar to previous changes (3)
  • internal/client/response.go
  • cmd/api/api_test.go
  • internal/client/api_errors.go

📝 Walkthrough

Walkthrough

Replace generic network error handling for API responses with specialized error wrappers that detect JSON decode failures and empty JSON bodies; add a new client error utility and tests asserting API errors include diagnostic messages and hints.

Changes

Cohort / File(s) Summary
API command
cmd/api/api.go, cmd/api/api_test.go
Replace direct output.ErrNetwork(...) wrapping of ac.DoAPI(...) errors with client.WrapDoAPIError(...); add a test TestApiCmd_InvalidJSONResponse_ShowsDiagnostic verifying invalid/empty JSON responses produce ExitAPI diagnostics mentioning --output.
Client response handling
internal/client/response.go, internal/client/response_test.go
HandleResponse now uses WrapJSONResponseParseError(err, resp.RawBody) for JSON parse failures; ParseJSONResponse preserves wrapped errors (%w) and tests assert EOF/empty-body behavior yields API-classified errors with exact diagnostic messages.
API error utilities
internal/client/api_errors.go, internal/client/api_errors_test.go
New api_errors.go introduces WrapDoAPIError and WrapJSONResponseParseError plus rawAPIJSONHint; tests validate classification of network vs JSON decode errors, EOF handling, and presence of user-facing diagnostic message/hint.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I nibble bytes and hop through logs,
When JSON tangles in twisty bogs.
I wrap the errors, leave hints that glow—
"Empty? Invalid?" the message will show.
Hop to --output, friend, and onward we go. 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 21.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: improving raw API diagnostics for handling invalid or empty JSON responses, which is the primary focus across all modified files.
Description check ✅ Passed The description includes all required template sections: a clear summary, enumerated changes, comprehensive test plan with verification steps, and related issue links. Details match the actual implementation changes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

Review ran into problems

🔥 Problems

Git: Failed to clone repository. Please run the @coderabbitai full review command to re-trigger a full review. If the issue persists, set path_filters to include or exclude specific files.


Comment @coderabbitai help to get the list of available commands and usage tips.

@CLAassistant
Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Apr 3, 2026

Greptile Summary

This PR improves lark-cli api diagnostics when an API endpoint returns an empty or non-JSON body that fails JSON parsing. Instead of a generic network error, users now see a targeted api_error with a helpful --output hint.

Key changes:

  • New internal/client/api_errors.go with WrapDoAPIError and WrapJSONResponseParseError helpers that classify json.SyntaxError, json.UnmarshalTypeError, and empty-body io.EOF as API-level diagnostic errors rather than network errors.
  • ParseJSONResponse in response.go changes %v to %w so the underlying decoder error is preserved in the chain, enabling errors.Is/errors.As to work correctly in the new helpers.
  • cmd/api/api.go routes DoAPI errors through WrapDoAPIError and HandleResponse parse errors through WrapJSONResponseParseError.
  • All three previously raised concerns (bare io.EOF misclassification in WrapDoAPIError, dead errors.Is/errors.As code due to %v wrapping, and fragile test assertion) have been addressed in this revision.

Confidence Score: 5/5

Safe to merge; all three previously raised concerns have been addressed and no new issues found.

All prior review concerns (bare io.EOF misclassification, dead errors.Is/errors.As code due to %v wrapping, and fragile test assertion) are resolved in this revision. No remaining P0 or P1 issues were found. The design correctly gates io.EOF/io.ErrUnexpectedEOF checks behind allowEOF, the %w change preserves the error chain as needed, and test coverage is solid for the new paths.

No files require special attention.

Important Files Changed

Filename Overview
internal/client/api_errors.go New helper file; cleanly separates JSON-decode errors from network errors with correct allowEOF gating and string-based fallbacks for SDK-wrapped errors.
internal/client/response.go Switches %v to %w in ParseJSONResponse (preserving the error chain) and routes parse failures through WrapJSONResponseParseError.
cmd/api/api.go Routes DoAPI errors through WrapDoAPIError; minimal, correct change.
internal/client/api_errors_test.go Good unit coverage: bare EOF → ExitNetwork, SyntaxError → ExitAPI diagnostic, UnexpectedEOF → ExitAPI diagnostic.
internal/client/response_test.go New tests verify empty-body wraps io.EOF (via %w) and that HandleResponse produces the correct ExitAPI diagnostic for an empty JSON body.
cmd/api/api_test.go Integration test now robustly accepts both 'invalid JSON response' and 'empty JSON response body' messages, addressing the prior review concern.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[lark-cli api GET /path] --> B[ac.DoAPI]
    B --> C{err != nil?}
    C -->|yes| D[WrapDoAPIError]
    D --> E{isJSONDecodeError allowEOF=false}
    E -->|SyntaxError or invalid character| F[ExitAPI + --output hint]
    E -->|bare io.EOF or other| G[ExitNetwork]
    C -->|no| H[HandleResponse]
    H --> I[ParseJSONResponse uses %w]
    I --> J{decode err?}
    J -->|yes| K[WrapJSONResponseParseError]
    K --> L{body empty?}
    L -->|yes| M[ExitAPI empty JSON body + hint]
    L -->|no| N{isJSONDecodeError allowEOF=true}
    N -->|yes| O[ExitAPI invalid JSON + hint]
    N -->|no| P[ExitNetwork]
    J -->|no| Q[CheckLarkResponse and output result]
Loading

Reviews (2): Last reviewed commit: "fix: tighten API JSON decode error class..." | Re-trigger Greptile

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/client/api_errors.go`:
- Around line 54-61: The JSON error classification currently checks for io.EOF
and string substrings but misses io.ErrUnexpectedEOF and the "unexpected EOF"
message and also fails when the original error is wrapped with "%v"; update the
error check block (the code that tests errors.Is(err, io.EOF) || errors.As(err,
&syntaxErr) || errors.As(err, &unmarshalTypeErr)) to also check errors.Is(err,
io.ErrUnexpectedEOF) and to test strings.Contains(msg, "unexpected EOF"), and
then change the error wrapping where the response error is created (the place
that uses fmt.Errorf("%v", err)) to use fmt.Errorf("%w", err) so errors.Is/As
still work when the error is wrapped; reference the existing variables err,
syntaxErr, unmarshalTypeErr when making these changes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 467a6e6f-7d55-42ad-bb06-6655db8d1c4f

📥 Commits

Reviewing files that changed from the base of the PR and between 0c77c95 and 96c5d8d.

📒 Files selected for processing (5)
  • cmd/api/api.go
  • cmd/api/api_test.go
  • internal/client/api_errors.go
  • internal/client/response.go
  • internal/client/response_test.go

Comment on lines +54 to +61
if errors.Is(err, io.EOF) || errors.As(err, &syntaxErr) || errors.As(err, &unmarshalTypeErr) {
return true
}

msg := err.Error()
return strings.Contains(msg, "unexpected end of JSON input") ||
strings.Contains(msg, "invalid character") ||
strings.Contains(msg, "cannot unmarshal")
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Check ParseJSONResponse wrapping style:"
rg -n 'response parse error: %.' internal/client/response.go -n -C2

echo
echo "Check JSON-decode detection branches:"
rg -n 'errors\.Is\(err, io\.(EOF|ErrUnexpectedEOF)\)|unexpected EOF|unexpected end of JSON input|invalid character|cannot unmarshal' internal/client/api_errors.go -n -C2

Repository: larksuite/cli

Length of output: 728


🏁 Script executed:

cat go.mod | head -5

Repository: larksuite/cli

Length of output: 110


🏁 Script executed:

rg -n 'fmt\.Errorf.*%v' internal/client/ --type go

Repository: larksuite/cli

Length of output: 202


🏁 Script executed:

rg -n 'json\.Decoder|dec\.Decode' internal/client/ --type go -A3

Repository: larksuite/cli

Length of output: 366


🌐 Web query:

Go json.Decoder truncated JSON error message "unexpected EOF"

💡 Result:

The "unexpected EOF" error from Go's json.Decoder typically indicates truncated or incomplete JSON input, where the parser reaches the end of available data before completing a valid JSON value (e.g., missing closing quote, bracket, or brace). Key causes and solutions: 1. Truncated input stream: Common with HTTP requests/bodies or pipes where data ends prematurely. - Ensure full body read: Use io.ReadAll(r.Body) then json.Unmarshal, or verify Content-Length matches. - Example from HTTP handler: Multiple requests (OPTIONS preflight + POST) exhaust body; check r.Method == "POST". 2. Syntax errors mimicking truncation: Unclosed strings/objects/arrays like {"key": "value trigger SyntaxError with "unexpected EOF" message. - Validate JSON: json.Valid(data) before decoding. - Source code shows decoder returns io.ErrUnexpectedEOF (wrapped as SyntaxError) on premature EOF during token scan. 3. Decoder vs Unmarshal difference: Decoder.Decode historically returned io.ErrUnexpectedEOF for truncated input; Unmarshal used SyntaxError{"unexpected end of JSON input"}. Issue #25956 discusses alignment (ongoing in json/v2). 4. Streaming edge cases: Pipes/files signal EOF after partial writes; recreate decoder or read fully. To debug: dec := json.NewDecoder(r) err := dec.Decode(&v) if err != nil { if serr, ok := err.(*json.SyntaxError); ok { fmt.Printf("Syntax error at offset %d: %v\n", serr.Offset, err) } } Use dec.InputOffset for error position. For strict validation, call dec.DisallowUnknownFields. Prevent via full reads or input validation.

Citations:


🏁 Script executed:

rg -n 'fmt\.Errorf' internal/client/ --type go | grep -i 'decode\|json\|parse'

Repository: larksuite/cli

Length of output: 202


🏁 Script executed:

python3 << 'EOF'
# Check what the error message for io.ErrUnexpectedEOF is
# by examining its string representation in Go stdlib

import subprocess
result = subprocess.run(['rg', '-n', 'ErrUnexpectedEOF', '.', '--type', 'go'], 
                       capture_output=True, text=True)
if result.stdout:
    print("Found ErrUnexpectedEOF references in repo:")
    print(result.stdout)
else:
    print("No ErrUnexpectedEOF references in repo (expected - it's stdlib)")

# Check if there are tests or examples showing JSON decoder errors
result2 = subprocess.run(['rg', '-n', 'Decode.*error|json.*decode.*error', 
                         'internal/client/', '--type', 'go', '-i'],
                        capture_output=True, text=True)
if result2.stdout:
    print("\nJSON decode error patterns in tests/code:")
    print(result2.stdout[:1000])
EOF

Repository: larksuite/cli

Length of output: 469


🏁 Script executed:

# Verify the actual error message from io.ErrUnexpectedEOF
python3 << 'EOF'
# In Go, io.ErrUnexpectedEOF has the message "unexpected EOF"
# This is confirmed by the Go stdlib source
# Let's verify response.go line 114 is the only place with this pattern

import subprocess
result = subprocess.run(['rg', '-n', 'Errorf.*parse', 'internal/client/', '--type', 'go'],
                       capture_output=True, text=True)
print("All parse error wrappings in internal/client:")
print(result.stdout)
EOF

Repository: larksuite/cli

Length of output: 249


Add checks for io.ErrUnexpectedEOF and "unexpected EOF" to detect truncated JSON.

Lines 54–61 miss truncated JSON responses that json.Decoder.Decode() emits as io.ErrUnexpectedEOF. Since response.go:114 wraps errors with %v instead of %w, the direct error type is lost and only substring matching remains. The fallback checks also omit "unexpected EOF", allowing truncated payloads to be misclassified as network errors.

Add the missing checks and fix the error wrapping:

Proposed fixes
diff --git a/internal/client/api_errors.go b/internal/client/api_errors.go
@@
-	if errors.Is(err, io.EOF) || errors.As(err, &syntaxErr) || errors.As(err, &unmarshalTypeErr) {
+	if errors.Is(err, io.EOF) ||
+		errors.Is(err, io.ErrUnexpectedEOF) ||
+		errors.As(err, &syntaxErr) ||
+		errors.As(err, &unmarshalTypeErr) {
 		return true
 	}
@@
-	return strings.Contains(msg, "unexpected end of JSON input") ||
+	return strings.Contains(msg, "unexpected EOF") ||
+		strings.Contains(msg, "unexpected end of JSON input") ||
 		strings.Contains(msg, "invalid character") ||
 		strings.Contains(msg, "cannot unmarshal")
diff --git a/internal/client/response.go b/internal/client/response.go
@@
-	if err := dec.Decode(&result); err != nil {
-		return nil, fmt.Errorf("response parse error: %v (body: %s)", err, util.TruncateStr(string(resp.RawBody), 500))
+	if err := dec.Decode(&result); err != nil {
+		return nil, fmt.Errorf("response parse error: %w (body: %s)", err, util.TruncateStr(string(resp.RawBody), 500))
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if errors.Is(err, io.EOF) || errors.As(err, &syntaxErr) || errors.As(err, &unmarshalTypeErr) {
return true
}
msg := err.Error()
return strings.Contains(msg, "unexpected end of JSON input") ||
strings.Contains(msg, "invalid character") ||
strings.Contains(msg, "cannot unmarshal")
if errors.Is(err, io.EOF) ||
errors.Is(err, io.ErrUnexpectedEOF) ||
errors.As(err, &syntaxErr) ||
errors.As(err, &unmarshalTypeErr) {
return true
}
msg := err.Error()
return strings.Contains(msg, "unexpected EOF") ||
strings.Contains(msg, "unexpected end of JSON input") ||
strings.Contains(msg, "invalid character") ||
strings.Contains(msg, "cannot unmarshal")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/client/api_errors.go` around lines 54 - 61, The JSON error
classification currently checks for io.EOF and string substrings but misses
io.ErrUnexpectedEOF and the "unexpected EOF" message and also fails when the
original error is wrapped with "%v"; update the error check block (the code that
tests errors.Is(err, io.EOF) || errors.As(err, &syntaxErr) || errors.As(err,
&unmarshalTypeErr)) to also check errors.Is(err, io.ErrUnexpectedEOF) and to
test strings.Contains(msg, "unexpected EOF"), and then change the error wrapping
where the response error is created (the place that uses fmt.Errorf("%v", err))
to use fmt.Errorf("%w", err) so errors.Is/As still work when the error is
wrapped; reference the existing variables err, syntaxErr, unmarshalTypeErr when
making these changes.

- Do not treat bare io.EOF from DoAPI as JSON parse path
- Treat ErrUnexpectedEOF / partial decode as JSON diagnostics in response path
- Wrap decode errors with %w in ParseJSONResponse for errors.Is
- Adjust tests and add api_errors unit coverage

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Made-with: Cursor
@vuth-dogo
Copy link
Copy Markdown
Author

Pushed follow-up commit a40a628 addressing the automated review feedback:

  • Greptile / EOF: WrapDoAPIError no longer classifies bare io.EOF as a JSON parse failure; added TestWrapDoAPIError_BareEOFIsNetworkError.
  • Error chain: ParseJSONResponse now uses %w for the decode error so errors.Is works (see TestParseJSONResponse_EmptyBody_WrapsEOF).
  • Tests: relaxed the api cmd assertion to accept either invalid-JSON or empty-body wording where appropriate; added api_errors_test.go for syntax / unexpected-EOF paths.
  • Unexpected EOF: WrapJSONResponseParseError treats io.ErrUnexpectedEOF, substring unexpected EOF, and EOF only in the response-parse path (allowEOF flag on isJSONDecodeError).

Validation (local, GOTOOLCHAIN=go1.23.8): go test ./cmd/api/... ./internal/client/..., make unit-test, golangci-lint --new-from-rev=origin/main — all pass.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 3, 2026

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
{}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/L Large or sensitive change across domains or core paths

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants