Skip to content

fix: correct exit codes and JSON error output#57

Merged
ran-codes merged 2 commits intomainfrom
fix/error-handling-output
Feb 20, 2026
Merged

fix: correct exit codes and JSON error output#57
ran-codes merged 2 commits intomainfrom
fix/error-handling-output

Conversation

@ran-codes
Copy link
Copy Markdown
Owner

@ran-codes ran-codes commented Feb 20, 2026

Summary

  • Detect Zenodo's 400 responses caused by invalid tokens and map to exit code 2 (auth error) instead of 1
  • Add LikelyAuthError() method on APIError to identify 400s with empty field messages (Zenodo's quirk for bad tokens)
  • Fix error output: plain text for table mode, structured JSON for json mode (no duplicates)

ELI5

When you use a bad API token, Zenodo says "validation error" instead of "unauthorized". The CLI now recognizes this pattern and gives you exit code 2 (auth error) with a hint to check your token, instead of a confusing exit code 1 with "size:" gibberish.

Also, when you use -o json, errors now come out as proper JSON instead of plain text.

Test results

Test Expected Result
records list --token "bad" Exit 2, auth hint Exit 2, "may be caused by invalid API token"
records get 99999999999 Exit 1, not-found hint Exit 1, "Record not found"
records list -o json --token "bad" JSON error on stderr {"status":400,"hint":"...","code":2}

Test plan

  • go build ./... passes
  • go test ./... passes
  • All three error handling scenarios from issue v0.1 User Testing #47 verified

Closes the error handling section of #47.

🤖 Generated with Claude Code

ran-codes and others added 2 commits February 20, 2026 09:33
Move error formatting from cli.Execute() to main() so errors are
printed once: as structured JSON when -o json, plain text otherwise.
Execute() now returns (error, outputFormat) instead of printing directly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Detect Zenodo's 400 responses caused by invalid tokens (LikelyAuthError)
  and map them to exit code 2 instead of 1
- Add hint for 400 auth errors suggesting token check

Fixes the three error handling test cases in issue #47:
1. Invalid token → exit code 2 with helpful hint
2. Invalid record ID → exit code 1 with "not found" hint (already worked)
3. -o json with bad token → structured JSON error on stderr

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings February 20, 2026 14:34
@ran-codes ran-codes changed the title fix: error output formatting — no duplicate messages, JSON-aware fix: correct exit codes and JSON error output Feb 20, 2026
@ran-codes ran-codes merged commit afeb083 into main Feb 20, 2026
3 checks passed
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes duplicate error output when using -o json by refactoring error formatting to be centralized in main(). Previously, cli.Execute() printed errors directly, and main() would also print structured JSON errors, causing duplication. The solution changes Execute() to return both the error and the resolved output format, allowing main() to handle all error formatting consistently.

Changes:

  • Refactored cli.Execute() to return (error, string) instead of printing errors directly
  • Centralized error formatting in main() with format-aware output (plain text or JSON)
  • Added LikelyAuthError() method to detect auth errors disguised as 400 responses from Zenodo
  • Updated exit code logic to return code 2 for likely auth errors

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.

File Description
internal/cli/root.go Modified Execute() to return error and output format instead of printing, removed duplicate error printing
cmd/zenodo/main.go Centralized error formatting with format-aware output, removed isJSONOutput() helper, added exit code handling for likely auth errors
internal/model/error.go Added LikelyAuthError() method to detect 400 responses that indicate auth issues, added hint for 400 status code

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +123 to +126
func Execute() (error, string) {
// Run PersistentPreRunE first via cobra, then return output format.
err := rootCmd.Execute()
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %s\n", err)
}
return err
return err, appCtx.Output
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

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

When PersistentPreRunE fails (e.g., config loading error on line 38), appCtx.Output will be an empty string (zero value). This means the outputFmt returned will be empty, causing error formatting to fall into the else branch, which may not be the intended behavior.

Consider resolving the output format from the flags directly in Execute() before calling rootCmd.Execute(), or provide a default value like "table" when appCtx.Output is empty.

Copilot uses AI. Check for mistakes.
json.NewEncoder(os.Stderr).Encode(errObj)
} else {
// Plain text error to stderr.
fmt.Fprintf(os.Stderr, "Error: %s\n", err)
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

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

The plain text error output only prints the error message without the hint. In contrast, the JSON error output includes the hint (lines 28-30). This creates an inconsistent user experience where hints are only shown when using JSON output.

Consider also printing the hint for plain text errors. For example, if there's an APIError with a hint, print both the error and the hint to stderr.

Suggested change
fmt.Fprintf(os.Stderr, "Error: %s\n", err)
fmt.Fprintf(os.Stderr, "Error: %s\n", err)
var apiErr *model.APIError
if errors.As(err, &apiErr) {
if hint := apiErr.Hint(); hint != "" {
fmt.Fprintf(os.Stderr, "Hint: %s\n", hint)
}
}

Copilot uses AI. Check for mistakes.
Comment on lines +50 to +62
func (e *APIError) LikelyAuthError() bool {
if e.Status != 400 {
return false
}
// Zenodo returns a "validation error" with empty field messages when the
// token is invalid on /deposit/depositions endpoints.
for _, d := range e.Errors {
if d.Field != "" && d.Message == "" {
return true
}
}
return false
}
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

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

The new LikelyAuthError() method lacks test coverage. Given that this codebase has comprehensive test coverage (as seen in internal/api/client_test.go with tests for APIError), consider adding unit tests for this new method.

Tests should cover:

  1. Returns false for non-400 status codes
  2. Returns true when a 400 error has Details with non-empty Field and empty Message
  3. Returns false when a 400 error has other Detail patterns (e.g., both Field and Message present)

Copilot uses AI. Check for mistakes.
switch e.Status {
case 400:
if e.LikelyAuthError() {
return "This may be caused by an invalid API token. Check your token with: zenodo config set token <TOKEN>"
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

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

The hint message uses a different format than the 401 and 403 cases. Line 31 says "Check your token with: zenodo config set token" while lines 35 and 37 use "Set it with:" and "Generate a new token at:". For consistency and clarity, consider using a more parallel structure. For example, "This may be caused by an invalid API token. Set it with: zenodo config set token <TOKEN>" to match the style of line 35.

Suggested change
return "This may be caused by an invalid API token. Check your token with: zenodo config set token <TOKEN>"
return "This may be caused by an invalid API token. Set it with: zenodo config set token <TOKEN>"

Copilot uses AI. Check for mistakes.
@ran-codes ran-codes deleted the fix/error-handling-output branch February 20, 2026 14:39
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.

2 participants