netbirdio/netbird · error

waiting for browser login failed: %v

Error message

waiting for browser login failed: %v

What it means

WaitToken blocks until the user completes authorization in the browser (device flow polls the IdP token endpoint; PKCE waits on a loopback callback). This error means the wait ended badly: the user code or PKCE grant expired, the user denied consent, the token endpoint kept erroring, or a.ctx was cancelled (caller stopping the flow). The browser URL was already handed to urlOpener.Open before the wait began.

Source

Thrown at client/android/login.go:227

	// profiles, not by logging out — logout keeps the email.
	if a.cfgPath != "" {
		if hint := readProfileEmail(a.cfgPath); hint != "" {
			if setter, ok := oAuthFlow.(loginHintSetter); ok {
				setter.SetLoginHint(hint)
			}
		}
	}

	flowInfo, err := oAuthFlow.RequestAuthInfo(context.TODO())
	if err != nil {
		return nil, fmt.Errorf("getting a request OAuth flow info failed: %v", err)
	}

	go urlOpener.Open(flowInfo.VerificationURIComplete, flowInfo.UserCode)

	tokenInfo, err := oAuthFlow.WaitToken(a.ctx, flowInfo)
	if err != nil {
		return nil, fmt.Errorf("waiting for browser login failed: %v", err)
	}

	return &tokenInfo, nil
}

View on GitHub (pinned to 93e97f4bf1)

Solutions

  1. Make sure urlOpener.Open actually launches the browser with flowInfo.VerificationURIComplete
  2. Keep the app process alive while WaitToken runs (foreground service on Android)
  3. If the code expired, simply retry Login — each attempt requests a new code
  4. Do not cancel a.ctx while a login is in flight

Example fix

// before: any wait failure is fatal to the UI
func (l *listener) OnError(err error) { l.showFatal(err) }

// after: classify — expired/denied codes are recoverable by restarting the flow
func (l *listener) OnError(err error) {
	msg := err.Error()
	if strings.Contains(msg, "waiting for browser login failed") {
		l.promptRetry() // re-running Login gets a fresh user code
		return
	}
	l.showFatal(err)
}
Defensive patterns

Strategy: retry

Validate before calling

// Before Login, verify the URLOpener will really open the verification URI;
// a no-op Open guarantees the code expires and WaitToken fails.
urlOpener.Open(uri, code) // must launch a browser / custom tab in production builds

Try / catch

// Classify the wait failure: expired code or transient polling error → restart Login; explicit denial → stop
if strings.Contains(err.Error(), "waiting for browser login failed") {
	if isAccessDenied(err) { showDenied() } else { promptRelogin() } // re-running Login gets a fresh code
}

Prevention

When it happens

Trigger: oAuthFlow.WaitToken(a.ctx, flowInfo) errors when the user never finishes or cancels the browser step before the code expires, the IdP returns access_denied or repeated polling errors, the network drops mid-poll, or a.ctx is cancelled while the login is in flight.

Common situations: User backgrounds the app and Android suspends the process, URLOpener.Open fails to launch the browser, user takes too long on the IdP login page, the app stops the client mid-login.

Related errors


AI-assisted analysis of netbirdio/netbird@93e97f4bf1 (2026-08-16). Data as JSON: /api/errors/6f6d436ebb6e7ae4. Report an issue: GitHub.