diff --git a/cmd/ninja/cmd/login.go b/cmd/ninja/cmd/login.go index d456e53..478a724 100644 --- a/cmd/ninja/cmd/login.go +++ b/cmd/ninja/cmd/login.go @@ -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", diff --git a/cmd/ninja/cmd/login_test.go b/cmd/ninja/cmd/login_test.go index 3037b23..bff8d4c 100644 --- a/cmd/ninja/cmd/login_test.go +++ b/cmd/ninja/cmd/login_test.go @@ -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") + } +}