decolua/9router · error · Error
startData.reason || startData.error || `Failed to start ${pr
Error message
startData.reason || startData.error || `Failed to start ${providerId} callback server` What it means
startProxyFlow (used by Trae/Windsurf-style providers) first calls GET /api/oauth/<providerId>/start-proxy to spin up a local dynamic-port callback server. If the request fails, reports success:false, or omits callbackUrl, the modal throws with the server's `reason` or `error`, else the generic `Failed to start <providerId> callback server` message. The callback server is required because these providers redirect to a localhost port that must exist before the authorize URL is opened.
Source
Thrown at src/shared/components/OAuthModal.js:188
setError(err.message);
setStep("error");
setPolling(false);
return;
}
}
setError("Authorization timeout");
setStep("error");
setPolling(false);
}, [provider, onSuccess]);
// Trae/Windsurf proxy OAuth flow: dynamic-port local callback → auto exchange.
const startProxyFlow = useCallback(async (providerId) => {
// 1. Start the local callback server (returns a dynamic port + callback URL).
const startRes = await fetch(`/api/oauth/${providerId}/start-proxy`);
const startData = await startRes.json();
if (!startRes.ok || !startData.success || !startData.callbackUrl) {
throw new Error(startData.reason || startData.error || `Failed to start ${providerId} callback server`);
}
// 2. Build the authorize URL with redirect_uri = proxy callback URL.
const authorizeUrl = new URL(`/api/oauth/${providerId}/authorize`, window.location.origin);
authorizeUrl.searchParams.set("redirect_uri", startData.callbackUrl);
const authRes = await fetch(authorizeUrl);
const authData = await authRes.json();
if (!authRes.ok) throw new Error(authData.error);
// 3. Register the session so the proxy can match the incoming callback.
// Zed also passes code_verifier (encodes the RSA private key for decrypt);
// sent via POST body so the private key never lands in URL/query logs.
const regBody = { state: authData.state };
if (authData.codeVerifier) regBody.codeVerifier = authData.codeVerifier;
await fetch(`/api/oauth/${providerId}/register-session`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(regBody),
});
// 4. Open popup; proxy auto-exchanges on callback, modal polls poll-status.View on GitHub (pinned to 90b52e06ff)
Solutions
- Check the response body's `reason`/`error` field (Network tab) — it names the concrete start-up failure.
- Verify the environment allows binding a localhost port for the callback server (containers/sandboxes often don't).
- Confirm the providerId matches a proxy-capable provider and that /api/oauth/<providerId>/start-proxy exists (a 404 means wrong id or stale build).
- Restart the dashboard server; a wedged previous callback server can prevent a new one from starting.
Example fix
// before
if (!startRes.ok || !startData.success || !startData.callbackUrl) {
throw new Error(startData.reason || startData.error || `Failed to start ${providerId} callback server`);
}
// after
if (!startRes.ok || !startData?.success || !startData?.callbackUrl) {
throw new Error(startData?.reason || startData?.error || `Failed to start ${providerId} callback server (HTTP ${startRes.status})`);
} Defensive patterns
Strategy: fallback
Validate before calling
// probe the endpoint before driving the flow
const startRes = await fetch(`/api/oauth/${providerId}/start-proxy`);
if (!startRes.ok) {
console.warn(`start-proxy unavailable for ${providerId} (HTTP ${startRes.status}) — falling back to manual code entry`);
openManualCodeStep(providerId);
return;
} Type guard
function isStartProxyOk(data) {
return typeof data === "object" && data !== null
&& data.success === true
&& typeof data.callbackUrl === "string" && data.callbackUrl.length > 0;
} Try / catch
try {
const startRes = await fetch(`/api/oauth/${providerId}/start-proxy`);
const startData = await startRes.json().catch(() => ({}));
if (!isStartProxyOk(startData)) {
throw new Error(startData.reason || startData.error || `Failed to start ${providerId} callback server (HTTP ${startRes.status})`);
}
} catch (err) {
setError(`${err.message}. If localhost callbacks are blocked (container/sandbox), use manual code entry instead.`);
} Prevention
- Ensure the runtime environment permits binding localhost ports.
- Fall back to manual-code entry when the proxy callback server can't start.
- Use exact provider slugs so the dynamic route resolves.
- Restart the dashboard if a stale callback server is holding ports.
When it happens
Trigger: The start-proxy route fails to bind a local port (port exhaustion, no loopback permission), the provider isn't configured for proxy OAuth, the route returns an error body without success/callbackUrl, or the dashboard server itself is unreachable (request throws before JSON parsing — in that case the generic fetch error, not this message, surfaces).
Common situations: Running in an environment where binding localhost ports is blocked (containers, some CI sandboxes); another process squats the port range; dashboard started without the OAuth proxy feature enabled; providerId typo'd so the dynamic route 404s.
Related errors
- [AutoPing] ${provider}:${conn.id}: refresh failed: ${e.messa
- Machine ID is required for Cursor API
- http2 module not available
- Kiro tool input changed fragment type
- Kiro tool input must be a JSON object
AI-assisted analysis of decolua/9router@90b52e06ff (2026-08-30).
Data as JSON: /api/errors/910d4cb88914bd5e.
Report an issue: GitHub.