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

  1. Check the response body's `reason`/`error` field (Network tab) — it names the concrete start-up failure.
  2. Verify the environment allows binding a localhost port for the callback server (containers/sandboxes often don't).
  3. Confirm the providerId matches a proxy-capable provider and that /api/oauth/<providerId>/start-proxy exists (a 404 means wrong id or stale build).
  4. 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

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


AI-assisted analysis of decolua/9router@90b52e06ff (2026-08-30). Data as JSON: /api/errors/910d4cb88914bd5e. Report an issue: GitHub.