-
Notifications
You must be signed in to change notification settings - Fork 0
refactor(oauth): fix backoff bug and clean up error handling #17
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 |
|---|---|---|
|
|
@@ -181,7 +181,8 @@ func initConfig() { | |
| retry.WithHTTPClient(baseHTTPClient), | ||
| ) | ||
| if err != nil { | ||
| panic(fmt.Sprintf("failed to create retry client: %v", err)) | ||
| fmt.Fprintf(os.Stderr, "Error: failed to create retry client: %v\n", err) | ||
| os.Exit(1) | ||
| } | ||
|
|
||
| // Initialize token store based on mode | ||
|
|
@@ -387,7 +388,7 @@ func run(ctx context.Context, d tui.Displayer) error { | |
| // Demonstrate automatic refresh on 401 | ||
| if err := makeAPICallWithAutoRefresh(ctx, &storage, d); err != nil { | ||
| // Check if error is due to expired refresh token | ||
| if err == ErrRefreshTokenExpired { | ||
| if errors.Is(err, ErrRefreshTokenExpired) { | ||
| d.ReAuthRequired() | ||
| storage, err = performDeviceFlow(ctx, d) | ||
| if err != nil { | ||
|
|
@@ -401,7 +402,6 @@ func run(ctx context.Context, d tui.Displayer) error { | |
| d.Fatal(err) | ||
| return err | ||
| } | ||
| d.APICallOK() | ||
| } else { | ||
| d.APICallFailed(err) | ||
| } | ||
|
|
@@ -477,13 +477,13 @@ func requestDeviceCode(ctx context.Context) (*oauth2.DeviceAuthResponse, error) | |
|
|
||
| // performDeviceFlow performs the OAuth device authorization flow | ||
| func performDeviceFlow(ctx context.Context, d tui.Displayer) (credstore.Token, error) { | ||
| // Only TokenURL and ClientID are used downstream; | ||
| // requestDeviceCode() builds its own request directly. | ||
| config := &oauth2.Config{ | ||
| ClientID: clientID, | ||
| Endpoint: oauth2.Endpoint{ | ||
| DeviceAuthURL: serverURL + endpointDeviceCode, | ||
| TokenURL: serverURL + endpointToken, | ||
| TokenURL: serverURL + endpointToken, | ||
| }, | ||
| Scopes: []string{"read", "write"}, | ||
| } | ||
|
|
||
| // Step 1: Request device code (with retry logic) | ||
|
|
@@ -527,7 +527,7 @@ func performDeviceFlow(ctx context.Context, d tui.Displayer) (credstore.Token, e | |
| } | ||
|
|
||
| // pollForTokenWithProgress polls for token while reporting progress via Displayer. | ||
| // Implements exponential backoff for slow_down errors per RFC 8628. | ||
| // Implements additive backoff (+5s) for slow_down errors per RFC 8628 §3.5. | ||
| func pollForTokenWithProgress( | ||
| ctx context.Context, | ||
| config *oauth2.Config, | ||
|
|
@@ -540,9 +540,8 @@ func pollForTokenWithProgress( | |
| interval = 5 // Default to 5 seconds per RFC 8628 | ||
| } | ||
|
|
||
| // Exponential backoff state | ||
| // Backoff state | ||
| pollInterval := time.Duration(interval) * time.Second | ||
| backoffMultiplier := 1.0 | ||
|
|
||
| pollTicker := time.NewTicker(pollInterval) | ||
| defer pollTicker.Stop() | ||
|
|
@@ -572,12 +571,8 @@ func pollForTokenWithProgress( | |
| continue | ||
|
|
||
| case oauthErrSlowDown: | ||
| // Server requests slower polling - increase interval | ||
| backoffMultiplier *= 1.5 | ||
| pollInterval = min( | ||
| time.Duration(float64(pollInterval)*backoffMultiplier), | ||
| 60*time.Second, | ||
| ) | ||
| // Server requests slower polling - add 5s per RFC 8628 §3.5 | ||
| pollInterval = min(pollInterval+5*time.Second, 60*time.Second) | ||
| pollTicker.Reset(pollInterval) | ||
| d.PollSlowDown(pollInterval) | ||
| continue | ||
|
|
@@ -829,26 +824,27 @@ func makeAPICallWithAutoRefresh( | |
| if err != nil { | ||
| return fmt.Errorf("API request failed: %w", err) | ||
| } | ||
| defer resp.Body.Close() | ||
|
|
||
| // If 401, try to refresh and retry | ||
| // If 401, drain and close the first response body to allow connection reuse, | ||
| // then refresh the token and retry. | ||
| if resp.StatusCode == http.StatusUnauthorized { | ||
| _, _ = io.Copy(io.Discard, resp.Body) | ||
| resp.Body.Close() | ||
|
|
||
|
Comment on lines
+828
to
+833
|
||
| d.AccessTokenRejected() | ||
|
|
||
| newStorage, err := refreshAccessToken(ctx, storage.RefreshToken, d) | ||
| if err != nil { | ||
| // If refresh token is expired, propagate the error to trigger device flow | ||
| if err == ErrRefreshTokenExpired { | ||
| if errors.Is(err, ErrRefreshTokenExpired) { | ||
| return ErrRefreshTokenExpired | ||
| } | ||
| return fmt.Errorf("refresh failed: %w", err) | ||
| } | ||
|
|
||
| // Update storage in memory | ||
| // Note: newStorage has already been saved to disk by refreshAccessToken() | ||
| storage.AccessToken = newStorage.AccessToken | ||
| storage.RefreshToken = newStorage.RefreshToken | ||
| storage.ExpiresAt = newStorage.ExpiresAt | ||
| *storage = newStorage | ||
|
|
||
| d.TokenRefreshedRetrying() | ||
|
|
||
|
|
@@ -869,6 +865,8 @@ func makeAPICallWithAutoRefresh( | |
| return fmt.Errorf("retry failed: %w", err) | ||
| } | ||
| defer resp.Body.Close() | ||
| } else { | ||
| defer resp.Body.Close() | ||
| } | ||
|
|
||
| body, err := io.ReadAll(resp.Body) | ||
|
|
||
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.
This block now implements a linear +5s backoff on
slow_down(per RFC 8628), but the surrounding function-level documentation still mentions “exponential backoff”. Please update the doc/comment wording to match the new behavior so future readers aren’t misled.