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
- Make sure urlOpener.Open actually launches the browser with flowInfo.VerificationURIComplete
- Keep the app process alive while WaitToken runs (foreground service on Android)
- If the code expired, simply retry Login — each attempt requests a new code
- 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
- Launch the browser immediately with VerificationURIComplete
- Run the login inside a foreground service so Android does not suspend the wait
- Never cancel the Auth context while a login is in flight
- Show the user code in-app in case the URI opens the wrong page
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
- failed to get OAuth flow: %v
- getting a request OAuth flow info failed: %v
- session extend already in progress
- session_idle_timeout must be positive for L4 services
- request_timeout must be positive for L4 services
AI-assisted analysis of netbirdio/netbird@93e97f4bf1 (2026-08-16).
Data as JSON: /api/errors/6f6d436ebb6e7ae4.
Report an issue: GitHub.