Compare commits

...

3 Commits

Author SHA1 Message Date
Luis Pater
03209b35c0 Merge branch 'router-for-me:main' into main 2025-12-20 00:02:07 +08:00
Luis Pater
bbd1fe890a Merge pull request #598 from BigUncle/fix/token-refresh-loop
fix(auth): prevent token refresh loop by ignoring timestamp fields
2025-12-19 23:59:40 +08:00
BigUncle
39597267ae fix(auth): prevent token refresh loop by ignoring timestamp fields
Add metadataEqualIgnoringTimestamps() function to compare metadata JSON
without timestamp/expired/expires_in/last_refresh/access_token fields.
This prevents unnecessary file writes when only these fields change
during refresh, breaking the fsnotify event → Watcher callback → refresh loop.

Key insight: Google OAuth returns a new access_token on each refresh,
which was causing file writes and triggering the refresh loop.

Fixes antigravity channel excessive log generation issue.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-18 21:37:05 +08:00

View File

@@ -72,7 +72,9 @@ func (s *FileTokenStore) Save(ctx context.Context, auth *cliproxyauth.Auth) (str
return "", fmt.Errorf("auth filestore: marshal metadata failed: %w", errMarshal)
}
if existing, errRead := os.ReadFile(path); errRead == nil {
if jsonEqual(existing, raw) {
// Use metadataEqualIgnoringTimestamps to skip writes when only timestamp fields change.
// This prevents the token refresh loop caused by timestamp/expired/expires_in changes.
if metadataEqualIgnoringTimestamps(existing, raw) {
return path, nil
}
} else if errRead != nil && !os.IsNotExist(errRead) {
@@ -264,6 +266,8 @@ func (s *FileTokenStore) baseDirSnapshot() string {
return s.baseDir
}
// DEPRECATED: Use metadataEqualIgnoringTimestamps for comparing auth metadata.
// This function is kept for backward compatibility but can cause refresh loops.
func jsonEqual(a, b []byte) bool {
var objA any
var objB any
@@ -276,6 +280,32 @@ func jsonEqual(a, b []byte) bool {
return deepEqualJSON(objA, objB)
}
// metadataEqualIgnoringTimestamps compares two metadata JSON blobs,
// ignoring fields that change on every refresh but don't affect functionality.
// This prevents unnecessary file writes that would trigger watcher events and
// create refresh loops.
func metadataEqualIgnoringTimestamps(a, b []byte) bool {
var objA, objB map[string]any
if err := json.Unmarshal(a, &objA); err != nil {
return false
}
if err := json.Unmarshal(b, &objB); err != nil {
return false
}
// Fields to ignore: these change on every refresh but don't affect authentication logic.
// - timestamp, expired, expires_in, last_refresh: time-related fields that change on refresh
// - access_token: Google OAuth returns a new access_token on each refresh, this is expected
// and shouldn't trigger file writes (the new token will be fetched again when needed)
ignoredFields := []string{"timestamp", "expired", "expires_in", "last_refresh", "access_token"}
for _, field := range ignoredFields {
delete(objA, field)
delete(objB, field)
}
return deepEqualJSON(objA, objB)
}
func deepEqualJSON(a, b any) bool {
switch valA := a.(type) {
case map[string]any: