Kuberwastaken/claurst · error

OAuth callback path mismatch: expected

Error message

OAuth callback path mismatch: expected '{}', got '{}'

What it means

During OAuth authorization-code flow, the library spins up a local callback server and waits for the identity provider to redirect to it. After writing the response page, it validates that the incoming request path equals the expected callback path; a mismatch means the request did not come from the expected redirect and is rejected.

Solutions

  1. Retry the auth flow and complete the browser login without editing the URL
  2. Ensure the redirect_uri registered with the OAuth provider matches the local callback path exactly
  3. Check that no other process is bound to the same callback port and sending requests
  4. Re-run the auth session so a fresh callback path/port is bound
Defensive patterns

Strategy: try-catch

Try / catch

match run_mcp_auth_session(server).await {
    Err(e) if e.to_string().contains("callback path mismatch") => {
        // restart flow; warn user not to touch the callback URL and check the port is free
    }
    other => other?,
}

Prevention

When it happens

Trigger: wait_for_authorization_code (invoked by run_mcp_auth_session) receives an HTTP request on the local callback port whose path differs from the registered callback_path.

Common situations: User or another app visited the callback URL directly; a port previously used by another tool now receives stray requests; the OAuth client's registered redirect URI path was changed server-side; browser extension or health-check probe hits the local server.

Related errors


AI-assisted analysis of Kuberwastaken/claurst@b0637c97ec (2026-09-10). Data as JSON: /api/errors/268376765e2f8535. Report an issue: GitHub.

Appendix: source

Thrown at src-rust/crates/mcp/src/oauth.rs:278

            .await
            .map_err(|e| anyhow::anyhow!("Failed to read OAuth callback headers: {}", e))?;
        if header.trim().is_empty() {
            break;
        }
    }

    let path = request_line.split_whitespace().nth(1).unwrap_or("");
    let parsed_url = url::Url::parse(&format!("http://{}{}", host, path))
        .map_err(|e| anyhow::anyhow!("Failed to parse OAuth callback URL '{}': {}", path, e))?;

    let response = "HTTP/1.1 200 OK\r\nContent-Type: text/plain; charset=utf-8\r\nConnection: close\r\n\r\nMCP OAuth authentication finished. You can close this tab.\r\n";
    writer
        .write_all(response.as_bytes())
        .await
        .map_err(|e| anyhow::anyhow!("Failed to write OAuth callback response: {}", e))?;

    if parsed_url.path() != callback_path {
        anyhow::bail!(
            "OAuth callback path mismatch: expected '{}', got '{}'",
            callback_path,
            parsed_url.path()
        );
    }

    if let Some(expected_state) = expected_state {
        let received_state = parsed_url
            .query_pairs()
            .find(|(key, _)| key == "state")
            .map(|(_, value)| value.to_string());
        if received_state.as_deref() != Some(expected_state) {
            anyhow::bail!("OAuth state mismatch — possible CSRF attack");
        }
    }

    parsed_url
        .query_pairs()

View on GitHub (pinned to b0637c97ec)