# login: fall back to the device flow when the loopback listener fails

`1663939`→[main](/content/gh/entireio/cli/commits/main/index.html)·  
  
Soph·1mo ago·2 files·+40 added/-12 removed

If StartBrowserAuth couldn't bind the 127.0.0.1 listener (sandboxing, firewall policy, exhausted ports), `entire login` errored out even though the device-code flow would have worked fine. Warn on stderr with the bind error and continue with the device flow instead of stranding the user.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

## Sessions

247398f9ecc7View transcript

## Changes

2

- cmd/entire/cli  
  
  - Mlogin.go+10/-5  
  
  - Mlogin_test.go+30/-7

```
77 unmodified lines

// device-code flows and runs the chosen one. The browser flow is the
// default — no code to type, no poll latency — but it needs a browser that
// can reach this machine's 127.0.0.1, so headless terminals (CI, piped
// stdin) and SSH sessions fall back to the device flow with a one-line
// explanation; the same both-flows-with-fallback shape gh / gcloud /
// aws sso ship. --device forces the device flow without commentary.
func runLoginAuto(ctx context.Context, outW, errW io.Writer, deviceClient deviceAuthClient, startBrowser func(context.Context) (browserAuthFlow, error), openURL browserOpenFunc, useDevice, canPrompt, sshSession bool) error {
    if shouldUseBrowserLogin(useDevice, canPrompt, sshSession) {
        flow, err := startBrowser(ctx)
        if err != nil {
            return fmt.Errorf("start login: %w", err)
            // Binding the loopback listener can fail (sandboxing, firewall,
            // exhausted ports); that shouldn't strand the user — warn and
            // use the device flow instead.
            fmt.Fprintf(errW, "Warning: could not start browser sign-in (%v); falling back to the device-code flow.\n", err)
            return runLogin(ctx, outW, errW, deviceClient, openURL)
        }
        return runBrowserLogin(ctx, outW, errW, flow, deviceClient.BaseURL(), openURL, browserLoginTimeout)
    }
}
```

375 unmodified lines

// startBrowserStub returns a startBrowser func that records invocations and
// returns the given flow.
func startBrowserStub(calls *int, flow browserAuthFlow) func(context.Context) (browserAuthFlow, error) {
    // returns the given flow/error.
    return func(context.Context) (browserAuthFlow, error) {
        *calls++
        return flow, nil
    }
}

var errW bytes.Buffer
err := runLoginAuto(context.Background(), &bytes.Buffer{}, &errW, &mockClient{},
    startBrowserStub(&browserCalls, flow), noopOpen,
    startBrowserStub(&browserCalls, flow, nil), noopOpen,
    false /* useDevice */, true /* canPrompt */, false /* ssh */)

if browserCalls != 1 {
    t.Errorf("startBrowser calls = %d, want 1", browserCalls)
}
if !strings.Contains(errW.String(), "could not start browser sign-in") {
    t.Errorf("stderr missing fallback warning:\n%s", errW.String())
}
// mockClient.StartDeviceAuth errors — proof the device flow was attempted.
if err == nil || !strings.Contains(err.Error(), "not implemented in mock") {
    t.Fatalf("err = %v, want device-flow start error from mock", err)
}

func TestRunLoginAuto_DeviceFlag_NoExplanation(t *testing.T) {
    t.Parallel()

var errW bytes.Buffer
    err := runLoginAuto(context.Background(), &bytes.Buffer{}, &errW, &mockClient{},
        startBrowserStub(&browserCalls, nil), noopOpen,
        startBrowserStub(&browserCalls, nil, nil), noopOpen,
        true /* useDevice */, true /* canPrompt */, false /* ssh */)
    if browserCalls != 0 {
