fix(login): retry transient poll errors instead of aborting the device flow

A proxy 502 during a backend restart killed attended logins mid-poll.
NotFound/InvalidArgument still fail fast; everything else retries until
the device-code deadline, with a one-time notice.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Alex Dunmow 2026-07-07 11:05:30 +08:00
parent 2b685348c9
commit 4e82b1ef41
2 changed files with 39 additions and 1 deletions

View File

@ -38,11 +38,19 @@ func newLoginCmd() *cobra.Command {
fmt.Printf("Visit %s?user_code=%s to authorize.\n", start.Msg.VerificationUri, start.Msg.UserCode)
interval := time.Duration(start.Msg.IntervalSeconds) * time.Second
deadline := time.Now().Add(time.Duration(start.Msg.ExpiresInSeconds) * time.Second)
warned := false
for time.Now().Before(deadline) {
time.Sleep(interval)
poll, err := cli.Auth.PollDevice(ctx, connect.NewRequest(&v1.PollDeviceRequest{DeviceCode: start.Msg.DeviceCode}))
if err != nil {
return err
if !pollRetryable(err) {
return err
}
if !warned {
fmt.Printf("Temporary error while waiting for approval (will keep trying): %v\n", err)
warned = true
}
continue
}
switch poll.Msg.Status {
case "pending":
@ -82,6 +90,18 @@ func newLoginCmd() *cobra.Command {
return cmd
}
// pollRetryable reports whether a PollDevice error is worth retrying until
// the device-code deadline. Transient transport failures (a proxy 502 while
// the backend restarts, timeouts) must not abort an attended login; NotFound
// and InvalidArgument mean the server can never approve this code.
func pollRetryable(err error) bool {
switch connect.CodeOf(err) {
case connect.CodeNotFound, connect.CodeInvalidArgument:
return false
}
return true
}
func newWhoamiCmd() *cobra.Command {
return &cobra.Command{
Use: "whoami",

View File

@ -1,8 +1,11 @@
package cmd
import (
"errors"
"testing"
"connectrpc.com/connect"
"git.dev.alexdunmow.com/block/cli/internal/creds"
)
@ -25,3 +28,18 @@ func TestApplyPolledAccount_EmptyFallsThrough(t *testing.T) {
t.Fatalf("hc = %+v, want untouched", hc)
}
}
func TestPollRetryable(t *testing.T) {
if pollRetryable(connect.NewError(connect.CodeNotFound, errors.New("unknown device code"))) {
t.Error("NotFound must not be retried — the server no longer knows the code")
}
if pollRetryable(connect.NewError(connect.CodeInvalidArgument, errors.New("device_code is required"))) {
t.Error("InvalidArgument must not be retried")
}
if !pollRetryable(connect.NewError(connect.CodeUnavailable, errors.New("unavailable: 502 Bad Gateway"))) {
t.Error("Unavailable (proxy 502 during backend restart) must be retried")
}
if !pollRetryable(errors.New("plain network error")) {
t.Error("non-connect errors must be retried")
}
}