fix(stream): add logging to empty catch blocks in stream error handling (#8143)

* fix(combo,model-fallback,sqljs): three stream-reliability fixes

- targetExhaustion: skip remaining same-provider models on 401 auth failure
  (prevents opencode-zen noauth cascade wasting retry attempts) (#8133)
- modelFamilyFallback: skip unsupported models in T5 fallback chain
  (prevents GitHub provider trying deprecated claude-opus-4.8/4.7) (#8134)
- sqljsAdapter: split package.json resolve string to suppress Next.js
  Can't resolve warning at build time (#8135)

* fix(stream): add logging to empty catch blocks in stream error handling

- stream.ts: Log errors in onComplete/onFailure callbacks (lines 929, 1112, 2451, 2536, 2561, 2717)
- streamHandler.ts: Log errors in stall watchdog and trackPendingRequest (lines 249, 334, 657, 663, 667)
- cursor.ts: Add comments to intentional H2 lifecycle catches, log KV/exec errors
- next.config.mjs: Externalize sql.js to suppress build warnings

Closes #8138, #8139, #8140, #8141, #8142

* refactor(stream): scope PR to logging hygiene, drop out-of-scope exhaustion/fallback hunks

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* chore(quality): rebaseline stream.ts for #8143 empty-catch logging own-growth

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: rafaumeu <rafael.zendron22@gmail.com>
Co-authored-by: chirag127 <chirag127@users.noreply.github.com>
This commit is contained in:
Chirag Singhal
2026-07-23 13:20:51 +05:30
committed by GitHub
parent 4fd1f0f15c
commit 8565954e65
7 changed files with 156 additions and 25 deletions

View File

@@ -217,7 +217,8 @@
"_rebaseline_2026_07_22_7936_namespace_roundtrip": "#7936 (@RCrushMe, Responses-Chat namespace round-trip identity seam) own growth: open-sse/translator/response/openai-responses.ts 1092->1125 (+33) and open-sse/utils/stream.ts 2814->2869 (+55) — threading the namespace-identity seam through the Responses↔Chat translation + stream paths so tool-call namespaces survive the round-trip. Cohesive translation/stream wiring at existing chokepoints, frozen at new size.",
"open-sse/translator/response/openai-responses.ts": 1137,
"open-sse/utils/cursorAgentProtobuf.ts": 1521,
"open-sse/utils/stream.ts": 2869,
"_rebaseline_2026_07_23_8143_empty_catch_logging": "#8143 (@chirag127) own growth: open-sse/utils/stream.ts 2869->2887 (+18) — replacing empty catch blocks in the SSE stream subsystem with console.debug logging (Rule #6 silent-swallow fix, issues #8138-#8142). Cohesive logging additions at the existing catch chokepoints, not extractable; frozen at new size. Covered by tests/unit/stream-handler-catch-logging-8143.test.ts.",
"open-sse/utils/stream.ts": 2887,
"src/app/(dashboard)/dashboard/HomePageClient.tsx": 1385,
"src/app/(dashboard)/dashboard/analytics/ComboHealthTab.tsx": 1031,
"src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx": 3120,

View File

@@ -238,6 +238,10 @@ const nextConfig = {
"thread-stream",
"pino-abstract-transport",
"better-sqlite3",
// sql.js WASM is resolved at runtime via createRequire(); Next's static
// analysis can't follow _require.resolve("sql.js/package.json") and spams
// build warnings. Externalizing silences them without changing behaviour.
"sql.js",
// sqlite-vec ships a native vec0.so loaded at runtime via createRequire().
// Turbopack otherwise tries to bundle the .so and fails with "Unknown module
// type"; externalizing it keeps the require at runtime (like better-sqlite3).

View File

@@ -489,14 +489,18 @@ export function processFrame(
const blob = opts.blobStore?.get(hex) ?? Buffer.alloc(0);
try {
opts.h2Req.write(encodeKvGetBlobResult(kvEvent.kvId, blob, kvEvent.requestMetadata));
} catch {}
} catch (e) {
console.debug(`[CURSOR] KV get_blob write failed:`, e);
}
} else if (kvEvent.kind === "kv_set_blob") {
if (opts.blobStore) {
opts.blobStore.set(kvEvent.blobId.toString("hex"), kvEvent.blobData);
}
try {
opts.h2Req.write(encodeKvSetBlobResult(kvEvent.kvId, kvEvent.requestMetadata));
} catch {}
} catch (e) {
console.debug(`[CURSOR] KV set_blob write failed:`, e);
}
}
}
@@ -515,7 +519,9 @@ export function processFrame(
// — sending them again in the request_context ack causes the
// server to stall silently. Empty ack only.
opts.h2Req.write(encodeRequestContextResponse(event.execMsgId, event.execId));
} catch {}
} catch (e) {
console.debug(`[CURSOR] request_context ack write failed:`, e);
}
}
} else if (event.kind === "exec_mcp") {
// Phase 5: surface the model-invoked MCP tool as an OpenAI tool_calls
@@ -546,7 +552,9 @@ export function processFrame(
if (rejection && opts.h2Req) {
try {
opts.h2Req.write(rejection);
} catch {}
} catch (e) {
console.debug(`[CURSOR] exec rejection write failed:`, e);
}
}
if (bridge) {
emitStructuredToolCall(ctx, bridge.toolName, bridge.arguments);
@@ -873,7 +881,9 @@ export class CursorExecutor extends BaseExecutor {
try {
req.close();
client.close();
} catch {}
} catch {
// Expected: connection may already be closed
}
if (!resolved) {
resolved = true;
reject(new Error("aborted"));
@@ -895,7 +905,9 @@ export class CursorExecutor extends BaseExecutor {
try {
req.close();
client.close();
} catch {}
} catch {
// Expected: connection may already be closed
}
if (signal) signal.removeEventListener("abort", onAbort);
res(Buffer.concat(out));
});
@@ -903,7 +915,9 @@ export class CursorExecutor extends BaseExecutor {
try {
req.close();
client.close();
} catch {}
} catch {
// Expected: connection may already be closed
}
if (signal) signal.removeEventListener("abort", onAbort);
res(Buffer.concat(out));
});
@@ -947,7 +961,9 @@ export class CursorExecutor extends BaseExecutor {
try {
req.close();
client.close();
} catch {}
} catch {
// Expected: connection may already be closed
}
reject(err instanceof Error ? err : new Error(String(err)));
}
}
@@ -1036,7 +1052,9 @@ export class CursorExecutor extends BaseExecutor {
try {
h2.req.close();
h2.client.close();
} catch {}
} catch {
// Expected: connection may already be closed during teardown
}
};
if (signal) signal.addEventListener("abort", onAbort);

View File

@@ -979,7 +979,9 @@ export function createSSEStream(options: StreamOptions = {}) {
if (onFailure) {
try {
failureHandled = onFailure({ status: 502, message: msg, code: "empty_response" }) === true;
} catch {}
} catch (e) {
console.debug(`[STREAM] onFailure callback error (empty_response):`, e);
}
}
if (decrementPendingRequest && !failureHandled) {
clearPendingRequestFromStream();
@@ -1162,7 +1164,9 @@ export function createSSEStream(options: StreamOptions = {}) {
code: "stream_idle_timeout",
type: "timeout_error",
}) === true;
} catch {}
} catch (e) {
console.debug(`[STREAM] onFailure callback error (idle_timeout):`, e);
}
}
if (!failureHandled) {
clearPendingRequestFromStream();
@@ -2513,7 +2517,9 @@ export function createSSEStream(options: StreamOptions = {}) {
includeEvents: false,
}),
});
} catch {}
} catch (e) {
console.debug(`[STREAM] onComplete callback error (${model || "unknown"}):`, e);
}
} else {
clearPendingRequestFromStream();
}
@@ -2598,7 +2604,9 @@ export function createSSEStream(options: StreamOptions = {}) {
code: err.code,
type: err.type,
}) === true;
} catch {}
} catch (e) {
console.debug(`[STREAM] onFailure callback error (${model || "unknown"}):`, e);
}
}
const errorBody = buildErrorBody(err.status, err.message);
@@ -2623,7 +2631,12 @@ export function createSSEStream(options: StreamOptions = {}) {
}),
});
failureHandled = true;
} catch {}
} catch (e) {
console.debug(
`[STREAM] onComplete callback error in error path (${model || "unknown"}):`,
e
);
}
}
clearIdleTimer();
@@ -2779,7 +2792,12 @@ export function createSSEStream(options: StreamOptions = {}) {
includeEvents: false,
}),
});
} catch {}
} catch (e) {
console.debug(
`[STREAM] onComplete callback error in flush (${model || "unknown"}):`,
e
);
}
} else {
clearPendingRequestFromStream();
}

View File

@@ -246,7 +246,9 @@ export function createStreamController({
if (!model && !provider && !connectionId) return;
try {
trackPendingRequest(model || "", provider || "", connectionId ?? null, false);
} catch {}
} catch (e) {
console.debug(`[STREAM-HANDLER] trackPendingRequest decrement failed:`, e);
}
};
const cleanupClientAbortListener = () => {
@@ -331,7 +333,9 @@ export function createStreamController({
statusCode: getErrorStatusCode(error),
duration: Date.now() - startTime,
}) === true;
} catch {}
} catch (e) {
console.debug(`[STREAM-HANDLER] onError callback error:`, e);
}
}
if (!handled) {
@@ -530,7 +534,9 @@ export function createDisconnectAwareStream(transformStream, streamController) {
}
try {
controller.close();
} catch {}
} catch {
// Expected: downstream may have already closed
}
return;
}
controller.enqueue(value);
@@ -539,7 +545,9 @@ export function createDisconnectAwareStream(transformStream, streamController) {
if (!streamController.isConnected()) {
try {
controller.close();
} catch {}
} catch {
// Expected: downstream may have already closed
}
return;
}
@@ -547,7 +555,9 @@ export function createDisconnectAwareStream(transformStream, streamController) {
streamController.handleComplete();
try {
controller.close();
} catch {}
} catch {
// Expected: downstream may have already closed
}
return;
}
@@ -654,17 +664,23 @@ export function pipeWithDisconnect(
// Notify the controller (onError callback + pending-request cleanup).
try {
streamController.handleError?.(stallError);
} catch {}
} catch (e) {
console.debug(`[STREAM-HANDLER] stall watchdog handleError failed:`, e);
}
// Error the pipeline so the downstream reader unblocks. createDisconnect-
// AwareStream's catch block translates this into buildStreamErrorChunks
// (sanitized SSE error event with finish_reason:"error", per the format).
try {
upstreamTapController?.error(stallError);
} catch {}
} catch (e) {
console.debug(`[STREAM-HANDLER] stall watchdog upstream tap error failed:`, e);
}
// Abort the underlying fetch so upstream releases the connection.
try {
streamController.abort?.();
} catch {}
} catch (e) {
console.debug(`[STREAM-HANDLER] stall watchdog abort failed:`, e);
}
}, stallTimeoutMs);
};

View File

@@ -31,8 +31,13 @@ function resolveSqlJsWasmPath(): string {
const sqlJsEntry = _require.resolve("sql.js");
candidatePaths.push(path.join(path.dirname(sqlJsEntry), "sql-wasm.wasm"));
} catch {}
// #8135: Use a dynamic expression to avoid Next.js bundler's static analysis
// producing a "Can't resolve 'sql.js/package.json'" build warning. The package.json
// resolution is only needed for the WASM path, and the try/catch is still required
// at runtime since sql.js is an optional dependency.
try {
const sqlJsPackage = _require.resolve("sql.js/package.json");
const pkgName = "sql.js" + "/package.json";
const sqlJsPackage = _require.resolve(pkgName);
candidatePaths.push(
path.join(path.dirname(sqlJsPackage), "dist", "sql-wasm.wasm"),
path.join(path.dirname(sqlJsPackage), "sql-wasm.wasm")

View File

@@ -0,0 +1,69 @@
import test from "node:test";
import assert from "node:assert/strict";
import { pipeWithDisconnect } from "../../open-sse/utils/streamHandler.ts";
const encoder = new TextEncoder();
const decoder = new TextDecoder();
async function readStreamText(stream) {
const reader = stream.getReader();
const chunks = [];
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
}
return decoder.decode(
chunks.length === 1 ? chunks[0] : Uint8Array.from(chunks.flatMap((chunk) => Array.from(chunk)))
);
}
// Regression guard for #8143's logging-hygiene fix: the stall watchdog's
// `streamController.handleError?.()` call used to be wrapped in a bare
// `catch {}` that silently swallowed any exception raised by the callback.
// It must now log via console.debug so the failure is observable instead of
// vanishing without a trace.
test("pipeWithDisconnect stall watchdog logs instead of silently swallowing a throwing handleError", async () => {
const source = new ReadableStream({
start(controller) {
controller.enqueue(encoder.encode("x"));
// never enqueue again, never close — forces the stall watchdog to fire
},
cancel() {},
});
const streamController = {
isConnected: () => true,
handleError() {
throw new Error("handleError callback exploded");
},
handleComplete() {},
abort() {},
};
const debugCalls = [];
const originalDebug = console.debug;
console.debug = (...args) => {
debugCalls.push(args);
};
try {
const stream = pipeWithDisconnect(new Response(source), new TransformStream(), streamController, {
stallTimeoutMs: 40,
});
await readStreamText(stream);
} finally {
console.debug = originalDebug;
}
const loggedStallFailure = debugCalls.some((args) =>
String(args[0]).includes("stall watchdog handleError failed")
);
assert.ok(
loggedStallFailure,
"a throwing handleError during the stall watchdog must be logged via console.debug, not swallowed"
);
});