Hmbown/CodeWhale · error
${compactRuntimeError(response.status, body)}
Error message
${compactRuntimeError(response.status, body)} What it means
streamTurnEvents in the Feishu bridge throws when the HTTP response that should carry the Server-Sent-Events turn stream has a non-OK status. The body is read with readJsonSafe and formatted via compactRuntimeError, so the message carries the Runtime's status and error payload.
Solutions
- Check the status and body in the thrown message: fix auth (re-derive tokens), URL, or payload accordingly.
- Verify the bridge's runtime base URL and auth env vars match the currently running `codewhale web` instance.
- Inspect Runtime logs for the 5xx cause if the status is a server error, and check the turn request body for malformed ids.
Example fix
// before: stale session headers after Runtime restart headers: authHeaders() // after: restart bridge alongside the Runtime so credentials regenerate // $ codewhale web && npm run start --prefix integrations/feishu-bridge
Defensive patterns
Strategy: try-catch
Validate before calling
const res = await fetch(streamUrl, { method: 'POST', headers: authHeaders() });
if (!res.ok) throw new Error(await res.text());
if (!res.headers.get('content-type')?.includes('text/event-stream')) throw new Error('not an SSE stream'); Type guard
function isRuntimeHttpError(err) {
return err instanceof Error && /\b\d{3}\b/.test(err.message);
} Try / catch
try {
await streamTurnEvents(payload);
} catch (e) {
console.error('[feishu] turn stream failed:', e.message);
await replyToFeishu('Backend error: ' + e.message.slice(0, 200));
} Prevention
- Restart the bridge whenever the Runtime restarts so auth tokens and base URL stay in sync.
- Verify the runtime URL with a lightweight GET before opening SSE streams.
- Validate turn payload fields (conversation/turn ids) before sending.
When it happens
Trigger: Calling streamTurnEvents when the Runtime rejects the turn-streaming request — 401 from stale auth headers, 404 from a wrong RUNTIME_URL path, 400 from an invalid turn payload, or 5xx from a Runtime crash.
Common situations: Feishu bridge env credentials out of sync with the running Runtime after a restart; wrong base URL/port in the bridge env; Runtime rejecting a malformed conversation/turn id built from a Feishu message.
Understand the failure class
Background: "API error: {status}" and "HTTP 401/403/404/429/5xx" errors: non-2xx HTTP responses explained — this error's family across 27 libraries.
Related errors
- ${compactRuntimeError(response.status, body)}
- ${compactRuntimeError(response.status, body)}
- ${compactRuntimeError(response.status, body)}
- Runtime API event response did not include a readable body
- SSE stream request failed: HTTP
AI-assisted analysis of Hmbown/CodeWhale@433685b202 (2026-09-15).
Data as JSON: /api/errors/890ed6936fabbbcb.
Report an issue: GitHub.
Appendix: source
Thrown at integrations/feishu-bridge/src/index.mjs:333
async function streamTurnEvents(chatId, threadId, turnId, sinceSeq) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), config.turnTimeoutMs);
let responseText = "";
let latestSeq = sinceSeq;
let sentProgressAt = Date.now();
try {
const response = await fetch(
`${config.runtimeUrl}/v1/threads/${encodeURIComponent(threadId)}/events?since_seq=${sinceSeq}`,
{
headers: authHeaders(),
signal: controller.signal
}
);
if (!response.ok) {
const body = await readJsonSafe(response);
throw new Error(compactRuntimeError(response.status, body));
}
for await (const event of readSse(response)) {
if (!event.data) continue;
const record = JSON.parse(event.data);
latestSeq = Math.max(latestSeq, Number(record.seq || 0));
await threadStore.patchChat(chatId, { lastSeq: latestSeq });
if (turnId && record.turn_id && record.turn_id !== turnId) continue;
if (record.event === "item.delta" && record.payload?.kind === "agent_message") {
responseText += record.payload.delta || "";
const now = Date.now();
if (responseText.length > config.maxReplyChars && now - sentProgressAt > 15000) {
await sendText(chatId, responseText.slice(0, config.maxReplyChars));
responseText = responseText.slice(config.maxReplyChars);
sentProgressAt = now;
}View on GitHub (pinned to 433685b202)