From 8565954e653adc61628a7abd16e7854b64273ee1 Mon Sep 17 00:00:00 2001 From: Chirag Singhal <76880977+chirag127@users.noreply.github.com> Date: Thu, 23 Jul 2026 13:20:51 +0530 Subject: [PATCH] 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 Co-authored-by: chirag127 --- config/quality/file-size-baseline.json | 3 +- next.config.mjs | 4 ++ open-sse/executors/cursor.ts | 36 +++++++--- open-sse/utils/stream.ts | 30 ++++++-- open-sse/utils/streamHandler.ts | 32 ++++++--- src/lib/db/adapters/sqljsAdapter.ts | 7 +- .../stream-handler-catch-logging-8143.test.ts | 69 +++++++++++++++++++ 7 files changed, 156 insertions(+), 25 deletions(-) create mode 100644 tests/unit/stream-handler-catch-logging-8143.test.ts diff --git a/config/quality/file-size-baseline.json b/config/quality/file-size-baseline.json index 0b50410363..c25f3ea39f 100644 --- a/config/quality/file-size-baseline.json +++ b/config/quality/file-size-baseline.json @@ -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, diff --git a/next.config.mjs b/next.config.mjs index fa46e3adbd..e78a586abb 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -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). diff --git a/open-sse/executors/cursor.ts b/open-sse/executors/cursor.ts index 57c810470a..1a5ae4ec8e 100644 --- a/open-sse/executors/cursor.ts +++ b/open-sse/executors/cursor.ts @@ -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); diff --git a/open-sse/utils/stream.ts b/open-sse/utils/stream.ts index c031ddf385..c28fde029d 100644 --- a/open-sse/utils/stream.ts +++ b/open-sse/utils/stream.ts @@ -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(); } diff --git a/open-sse/utils/streamHandler.ts b/open-sse/utils/streamHandler.ts index eb7bd792ac..51ca1cc7b2 100644 --- a/open-sse/utils/streamHandler.ts +++ b/open-sse/utils/streamHandler.ts @@ -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); }; diff --git a/src/lib/db/adapters/sqljsAdapter.ts b/src/lib/db/adapters/sqljsAdapter.ts index d522913957..eaa21a134b 100644 --- a/src/lib/db/adapters/sqljsAdapter.ts +++ b/src/lib/db/adapters/sqljsAdapter.ts @@ -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") diff --git a/tests/unit/stream-handler-catch-logging-8143.test.ts b/tests/unit/stream-handler-catch-logging-8143.test.ts new file mode 100644 index 0000000000..06f172fe3b --- /dev/null +++ b/tests/unit/stream-handler-catch-logging-8143.test.ts @@ -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" + ); +});