Skip to content

Commit 7c1070a

Browse files
dydxclaude
andauthored
Fix authentication retry mechanism (#9)
* Fix authentication retry mechanism Improve token refresh logic and error handling in the authentication process, ensuring proper request retries with new tokens. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * gofmt -w . --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 4754e1a commit 7c1070a

1 file changed

Lines changed: 141 additions & 23 deletions

File tree

pkg/auth/auth.go

Lines changed: 141 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,32 @@ import (
1212
"io"
1313
"net/http"
1414
"os"
15+
"strings"
1516

1617
"github.com/dydx/vico-cli/pkg/cache"
1718
)
1819

1920
// Error codes from the API
2021
const (
21-
ErrorAccountKicked = -1024
22-
ErrorTokenMissing = -1025
22+
ErrorAccountKicked = -1024 // Account has been kicked offline
23+
ErrorTokenMissing = -1025 // Token is missing
24+
ErrorTokenInvalid = -1026 // Token is invalid
25+
ErrorTokenExpired = -1027 // Token has expired
2326
)
2427

28+
// isDebugMode returns true if debug logging is enabled
29+
func isDebugMode() bool {
30+
debug := os.Getenv("VICOHOME_DEBUG")
31+
return debug == "true" || debug == "1"
32+
}
33+
34+
// logDebug prints a message only if debug mode is enabled
35+
func logDebug(format string, args ...interface{}) {
36+
if isDebugMode() {
37+
fmt.Fprintf(os.Stderr, format, args...)
38+
}
39+
}
40+
2541
// LoginRequest represents the JSON request body sent to the Vicohome API
2642
// during authentication.
2743
type LoginRequest struct {
@@ -55,16 +71,19 @@ func Authenticate() (string, error) {
5571
cacheManager, err := cache.NewTokenCacheManager()
5672
if err != nil {
5773
// If we can't create a cache manager, fall back to direct authentication
74+
logDebug("Warning: Could not create token cache manager: %v\n", err)
5875
return authenticateDirectly()
5976
}
6077

6178
token, valid := cacheManager.GetToken()
6279
if valid {
80+
logDebug("Using cached token\n")
6381
// We have a valid cached token, return it
6482
return token, nil
6583
}
6684

6785
// No valid cached token, authenticate and cache the new token
86+
logDebug("No valid cached token found, authenticating directly\n")
6887
token, err = authenticateDirectly()
6988
if err != nil {
7089
return "", err
@@ -73,7 +92,9 @@ func Authenticate() (string, error) {
7392
// Cache the token for future use (24 hours validity)
7493
if err := cacheManager.SaveToken(token, 24); err != nil {
7594
// Non-fatal error, we can still return the token
76-
fmt.Fprintf(os.Stderr, "Warning: failed to cache token: %v\n", err)
95+
logDebug("Warning: failed to cache token: %v\n", err)
96+
} else {
97+
logDebug("Successfully cached new token\n")
7798
}
7899

79100
return token, nil
@@ -170,30 +191,80 @@ func authenticateDirectly() (string, error) {
170191
// - bool: True if the token needs to be refreshed, false otherwise
171192
// - error: Any error found in the response, or nil if no error was found
172193
func ValidateResponse(respBody []byte) (bool, error) {
194+
// Check if we have a non-JSON response (probably HTML error page)
195+
if len(respBody) > 0 && (respBody[0] == '<' || respBody[0] == '\r' || respBody[0] == '\n') {
196+
// Print a preview for debugging
197+
if isDebugMode() {
198+
preview := string(respBody)
199+
if len(preview) > 100 {
200+
preview = preview[:100] + "..."
201+
}
202+
logDebug("Warning: Received non-JSON response (likely auth issue): %s\n", preview)
203+
}
204+
return true, fmt.Errorf("received non-JSON response (likely authentication issue)")
205+
}
206+
173207
// Try to parse the response
174208
var responseMap map[string]interface{}
175209
if err := json.Unmarshal(respBody, &responseMap); err != nil {
176210
return false, fmt.Errorf("error unmarshaling response: %w", err)
177211
}
178212

179-
// Check for authentication errors
213+
// Print the response for debugging
214+
if isDebugMode() {
215+
prettyJSON, _ := json.MarshalIndent(responseMap, "", " ")
216+
logDebug("API Response: %s\n", string(prettyJSON))
217+
}
218+
219+
// Check for authentication errors - try both result and code fields (API inconsistency)
220+
var errorCode float64
221+
var errorMsg string
222+
var hasError bool
223+
224+
// Check the "result" field first (main API)
180225
if result, ok := responseMap["result"].(float64); ok {
226+
errorCode = result
181227
msg, _ := responseMap["msg"].(string)
228+
errorMsg = msg
229+
hasError = result != 0
230+
}
182231

183-
// Check if we need to refresh the token
184-
if result == ErrorAccountKicked || result == ErrorTokenMissing {
185-
// Clear the cache
186-
cacheManager, err := cache.NewTokenCacheManager()
187-
if err == nil {
188-
cacheManager.ClearToken()
189-
}
190-
return true, fmt.Errorf("authentication error: %s (code: %.0f)", msg, result)
232+
// Also check the "code" field (some endpoints use this instead)
233+
if code, ok := responseMap["code"].(float64); ok {
234+
errorCode = code
235+
msg, _ := responseMap["msg"].(string)
236+
errorMsg = msg
237+
hasError = code != 0
238+
}
239+
240+
// If we found an error code
241+
if hasError {
242+
// Check if it's an auth error requiring token refresh
243+
isAuthError := errorCode == ErrorAccountKicked ||
244+
errorCode == ErrorTokenMissing ||
245+
errorCode == ErrorTokenInvalid ||
246+
errorCode == ErrorTokenExpired
247+
248+
// Also check message strings for auth-related errors
249+
if !isAuthError && errorMsg != "" {
250+
errorMsgLower := strings.ToLower(errorMsg)
251+
isAuthError = strings.Contains(errorMsgLower, "token") ||
252+
strings.Contains(errorMsgLower, "auth") ||
253+
strings.Contains(errorMsgLower, "login") ||
254+
strings.Contains(errorMsgLower, "账号") || // account in Chinese
255+
strings.Contains(errorMsgLower, "踢下线") // kicked offline in Chinese
191256
}
192257

193-
// Check for other API errors
194-
if result != 0 {
195-
return false, fmt.Errorf("API error: %s (code: %.0f)", msg, result)
258+
if isAuthError {
259+
if isDebugMode() {
260+
logDebug("Auth error detected: %s (code: %.0f)\n", errorMsg, errorCode)
261+
}
262+
// Don't clear cache here, let the caller handle it
263+
return true, fmt.Errorf("authentication error: %s (code: %.0f)", errorMsg, errorCode)
196264
}
265+
266+
// Otherwise it's a regular API error
267+
return false, fmt.Errorf("API error: %s (code: %.0f)", errorMsg, errorCode)
197268
}
198269

199270
return false, nil
@@ -213,6 +284,19 @@ func ValidateResponse(respBody []byte) (bool, error) {
213284
func ExecuteWithRetry(req *http.Request) ([]byte, error) {
214285
// First attempt with current token
215286
client := &http.Client{}
287+
288+
// Make sure we can reuse the request body if needed
289+
var requestBodyBytes []byte
290+
if req.Body != nil {
291+
var err error
292+
requestBodyBytes, err = io.ReadAll(req.Body)
293+
if err != nil {
294+
return nil, fmt.Errorf("error reading request body: %w", err)
295+
}
296+
req.Body = io.NopCloser(bytes.NewBuffer(requestBodyBytes))
297+
}
298+
299+
// First attempt
216300
resp, err := client.Do(req)
217301
if err != nil {
218302
return nil, fmt.Errorf("error making request: %w", err)
@@ -225,30 +309,57 @@ func ExecuteWithRetry(req *http.Request) ([]byte, error) {
225309
}
226310

227311
// Check if we need to refresh the token
228-
needsRefresh, _ := ValidateResponse(respBody)
312+
needsRefresh, apiErr := ValidateResponse(respBody)
229313
if needsRefresh {
314+
// Only show detailed logs in debug mode
315+
logDebug("Token refresh needed: %v\n", apiErr)
316+
230317
// Clear the cache and get a new token
231318
cacheManager, err := cache.NewTokenCacheManager()
232319
if err == nil {
233320
cacheManager.ClearToken()
234321
}
235322

236-
// Get a new token
323+
// Get a new token directly (bypass cache)
237324
token, err := authenticateDirectly()
238325
if err != nil {
239326
return nil, fmt.Errorf("failed to refresh token: %w", err)
240327
}
241328

242-
// Cache the new token
329+
// Cache the new token with verbose error handling
243330
if cacheManager != nil {
244-
cacheManager.SaveToken(token, 24)
331+
if err := cacheManager.SaveToken(token, 24); err != nil {
332+
logDebug("Warning: failed to cache refreshed token: %v\n", err)
333+
} else {
334+
logDebug("Successfully refreshed and cached new token\n")
335+
}
336+
}
337+
338+
// Create a new request with the same parameters but new token
339+
newReq, err := http.NewRequest(req.Method, req.URL.String(), nil)
340+
if err != nil {
341+
return nil, fmt.Errorf("error creating request for retry: %w", err)
342+
}
343+
344+
// Copy all headers from the original request
345+
for key, values := range req.Header {
346+
for _, value := range values {
347+
newReq.Header.Add(key, value)
348+
}
245349
}
246350

247-
// Update the request with the new token
248-
req.Header.Set("Authorization", token)
351+
// Set the new Authorization header
352+
newReq.Header.Set("Authorization", token)
249353

250-
// Retry the request
251-
resp, err = client.Do(req)
354+
// Add back the body if there was one
355+
if len(requestBodyBytes) > 0 {
356+
newReq.Body = io.NopCloser(bytes.NewBuffer(requestBodyBytes))
357+
newReq.ContentLength = int64(len(requestBodyBytes))
358+
}
359+
360+
// Retry the request with the new token
361+
logDebug("Retrying request with refreshed token\n")
362+
resp, err = client.Do(newReq)
252363
if err != nil {
253364
return nil, fmt.Errorf("error making request after token refresh: %w", err)
254365
}
@@ -258,6 +369,13 @@ func ExecuteWithRetry(req *http.Request) ([]byte, error) {
258369
if err != nil {
259370
return nil, fmt.Errorf("error reading response body after token refresh: %w", err)
260371
}
372+
373+
// Check if we still got an error after refreshing the token
374+
needsRefresh2, apiError := ValidateResponse(respBody)
375+
if needsRefresh2 || apiError != nil {
376+
// This is a critical error worth showing - authentication failed even after token refresh
377+
return nil, fmt.Errorf("authentication failed even after token refresh: %v", apiError)
378+
}
261379
}
262380

263381
return respBody, nil

0 commit comments

Comments
 (0)