Problem
When connecting to the Gemini Live API via WebSocket, connection errors log an empty object with no useful information:
ws.onerror = (event) => {
console.error("[Gemini] WebSocket error:", event);
// Output: [Gemini] WebSocket error: {}
setError("Connection error");
};
The onerror handler receives an Event object, but console.error(event) produces {} because WebSocket error events have no enumerable properties. This makes it impossible to determine whether the failure is caused by an invalid API key, network issue, CORS policy, or protocol mismatch.
Solution
Step 1: Log meaningful connection state instead of the opaque event
ws.addEventListener("error", () => {
console.error("[Gemini] WebSocket error", {
readyState: ws.readyState,
url: ws.url.replace(/key=.*/, "key=REDACTED"),
protocol: ws.protocol,
bufferedAmount: ws.bufferedAmount,
});
});
Step 2: Use the close event to get the real error information
ws.addEventListener("close", (event) => {
console.error("[Gemini] WebSocket closed", {
code: event.code,
reason: event.reason,
wasClean: event.wasClean,
});
// Common codes:
// 1000 = normal closure
// 1006 = abnormal (network failure, no close frame)
// 1008 = policy violation (invalid API key or token)
// 1011 = server error
});
Step 3: Use ephemeral tokens for client-side WebSocket auth
// app/api/gemini-token/route.ts (server-side)
export async function POST() {
const response = await fetch(
"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-exp:generateContent",
{
method: "POST",
headers: {
"Content-Type": "application/json",
"x-goog-api-key": process.env.GEMINI_API_KEY!,
},
body: JSON.stringify({ contents: [{ parts: [{ text: "." }] }] }),
}
);
const data = await response.json();
return Response.json(data);
}
// Client-side: use the ephemeral token instead of the raw API key
const token = await fetch("/api/gemini-token", { method: "POST" }).then((r) =>
r.json()
);
const ws = new WebSocket(
`wss://generativelanguage.googleapis.com/ws/google.ai.generativelanguage.v1beta.GenerativeService.BidiGenerateContent?key=${token.key}`
);
Why It Works
The WebSocket error event is intentionally opaque per the W3C specification. This prevents information leakage about the network topology in cross-origin scenarios. The real diagnostic information comes from the close event, which fires after error and includes a numeric code and string reason. Code 1006 indicates the connection was never properly established (network failure), while 1008 signals a policy violation (typically an invalid or expired API key).
Logging readyState at the time of error distinguishes between connection-phase failures (readyState 0 = CONNECTING) and mid-session failures (readyState 1 = OPEN). Ephemeral tokens prevent API key exposure in client-side code and provide better error messages when auth fails.
Context
- Google Gemini Live API (multimodal streaming via WebSocket)
- Next.js 14+ with App Router for the token exchange endpoint
- The
errorevent is always followed by acloseevent -- always implement both handlers - Common close codes: 1000 (normal), 1006 (abnormal/network), 1008 (policy/auth), 1011 (server error)
- Ephemeral tokens expire after ~1 minute; request a new one before each session