mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
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:
@@ -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,
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
69
tests/unit/stream-handler-catch-logging-8143.test.ts
Normal file
69
tests/unit/stream-handler-catch-logging-8143.test.ts
Normal 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"
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user