-
Notifications
You must be signed in to change notification settings - Fork 0
Add error handling for Claude Code quota API failures #251
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -2,6 +2,7 @@ package daemon | |||||||||||||
|
|
||||||||||||||
| import ( | ||||||||||||||
| "context" | ||||||||||||||
| "fmt" | ||||||||||||||
| "log/slog" | ||||||||||||||
| "path/filepath" | ||||||||||||||
| "runtime" | ||||||||||||||
|
|
@@ -417,18 +418,25 @@ func (s *CCInfoTimerService) fetchRateLimit(ctx context.Context) { | |||||||||||||
| token, err := fetchClaudeCodeOAuthToken() | ||||||||||||||
| if err != nil || token == "" { | ||||||||||||||
| slog.Debug("Failed to get Claude Code OAuth token", slog.Any("err", err)) | ||||||||||||||
| s.rateLimitCache.mu.Lock() | ||||||||||||||
| s.rateLimitCache.lastError = "oauth" | ||||||||||||||
| s.rateLimitCache.mu.Unlock() | ||||||||||||||
| return | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| usage, err := fetchAnthropicUsage(ctx, token) | ||||||||||||||
| if err != nil { | ||||||||||||||
| slog.Warn("Failed to fetch Anthropic usage", slog.Any("err", err)) | ||||||||||||||
| s.rateLimitCache.mu.Lock() | ||||||||||||||
| s.rateLimitCache.lastError = shortenAPIError(err) | ||||||||||||||
| s.rateLimitCache.mu.Unlock() | ||||||||||||||
| return | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| s.rateLimitCache.mu.Lock() | ||||||||||||||
| s.rateLimitCache.usage = usage | ||||||||||||||
| s.rateLimitCache.fetchedAt = time.Now() | ||||||||||||||
| s.rateLimitCache.lastError = "" | ||||||||||||||
| s.rateLimitCache.mu.Unlock() | ||||||||||||||
|
|
||||||||||||||
| // Send usage data to server for push notification scheduling (fire-and-forget) | ||||||||||||||
|
|
@@ -535,3 +543,28 @@ func (s *CCInfoTimerService) GetCachedRateLimit() *AnthropicRateLimitData { | |||||||||||||
| copy := *s.rateLimitCache.usage | ||||||||||||||
| return © | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| // GetCachedRateLimitError returns the last error from rate limit fetching, or empty string if none. | ||||||||||||||
| func (s *CCInfoTimerService) GetCachedRateLimitError() string { | ||||||||||||||
| s.rateLimitCache.mu.RLock() | ||||||||||||||
| defer s.rateLimitCache.mu.RUnlock() | ||||||||||||||
| return s.rateLimitCache.lastError | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| // shortenAPIError converts an Anthropic usage API error into a short string for statusline display. | ||||||||||||||
| func shortenAPIError(err error) string { | ||||||||||||||
| msg := err.Error() | ||||||||||||||
|
|
||||||||||||||
| // Check for HTTP status code pattern | ||||||||||||||
| var status int | ||||||||||||||
| if _, scanErr := fmt.Sscanf(msg, "anthropic usage API returned status %d", &status); scanErr == nil { | ||||||||||||||
| return fmt.Sprintf("api:%d", status) | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| // Decode errors | ||||||||||||||
| if len(msg) >= 6 && msg[:6] == "failed" { | ||||||||||||||
| return "api:decode" | ||||||||||||||
| } | ||||||||||||||
|
Comment on lines
+565
to
+567
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. While parsing error strings works, it can be brittle if the error message format changes. Using For a more robust long-term solution, consider introducing typed errors. This would allow you to use You will need to add
Suggested change
|
||||||||||||||
|
|
||||||||||||||
| return "network" | ||||||||||||||
| } | ||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 lastError not cleared in stopTimer, causing stale error display after inactivity restart
The
stopTimer()method atdaemon/cc_info_timer.go:150-154clearsusage,fetchedAt, andlastAttemptAtfrom the rate limit cache, but does not clear the newly addedlastErrorfield. After the timer stops due to inactivity and a new client triggers a restart,GetCachedRateLimit()returns nil (usage was cleared) whileGetCachedRateLimitError()returns a stale error string from the previous session. This causeshandleCCInfoatdaemon/socket.go:257-258to surface a stale error in the statusline, even though the previous error may no longer be relevant.(Refers to lines 150-154)
Was this helpful? React with 👍 or 👎 to provide feedback.