Hmbown/CodeWhale · error
sign-in timed out waiting for the browser callback
Error message
{} sign-in timed out waiting for the browser callback What it means
The local loopback HTTP listener waited `CALLBACK_TIMEOUT` for the browser to complete sign-in and call back with the OAuth code, and the deadline expired. The library polls all resolved localhost stacks for the redirect; if none arrives in time the login is abandoned with this error.
Solutions
- Re-run the sign-in and complete the browser flow before the timeout.
- Open the printed authorize URL manually in a local browser if none launched (e.g. over SSH, use port forwarding).
- Verify nothing else occupies the loopback callback port and that a firewall/proxy isn't blocking `localhost` callbacks.
Defensive patterns
Strategy: retry
Try / catch
match pkce_login(provider).await {
Err(e) if e.to_string().contains("timed out waiting for the browser callback") => {
// retry once, or instruct user to open the printed URL manually
}
other => other,
} Prevention
- Ensure a graphical browser is available (no headless/SSH without port forwarding).
- Complete sign-in soon after the browser opens; the wait window is bounded.
- Confirm the loopback callback port is free and not firewalled.
When it happens
Trigger: `pkce_login`'s callback wait loop reaches `Instant::now() >= deadline` before any callback request is received — the browser never opened, the user never finished sign-in, or the redirect went to the wrong port/loopback family.
Common situations: Browser failed to launch in a headless/SSH environment; user walked away and didn't complete sign-in; popup blocker swallowed the tab; the authorize URL's redirect port was blocked by another process or firewall; slow provider login page exceeded the timeout.
Understand the failure class
Background: Request timed out: what client-side request timeouts mean across libraries (Request timed out, TIMED_OUT, APITimeoutError) — this error's family across 39 libraries.
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
- {}
- {message}
- {timeout_message}
- agy OAuth token JSON carries no access token member
- agy OAuth token member
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/1f37fa460f936f1f.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/src/oauth.rs:1373
pkce,
redirect_uri,
authorize_url,
})
}
const CALLBACK_TIMEOUT: Duration = Duration::from_secs(300);
const CALLBACK_HTML_OK: &str = "<!doctype html><html><body><p>Signed in to Codewhale. You can close this tab.</p></body></html>";
const CALLBACK_HTML_ERR: &str = "<!doctype html><html><body><p>Sign-in did not complete. You can close this tab and retry in Codewhale.</p></body></html>";
fn wait_for_callback(
listeners: &[TcpListener],
params: &OAuthProviderParams,
expected_state: &str,
) -> Result<String> {
let deadline = Instant::now() + CALLBACK_TIMEOUT;
loop {
if Instant::now() >= deadline {
bail!(
"{} sign-in timed out waiting for the browser callback",
params.display_name
);
}
// Whichever stack `localhost` resolved to for the browser is the one
// that gets the connection; poll them all.
for listener in listeners {
match listener.accept() {
Ok((stream, _)) => {
return handle_callback_stream(stream, params, expected_state);
}
Err(error)
if error.kind() == std::io::ErrorKind::WouldBlock
|| error.kind() == std::io::ErrorKind::Interrupted => {}
Err(error) => {
return Err(error).context(format!(
"{} OAuth callback accept failed",
params.display_nameView on GitHub (pinned to 73e0f67d83)