diff --git a/lib/structured-output.mjs b/lib/structured-output.mjs index a444bd9..238e7fb 100644 --- a/lib/structured-output.mjs +++ b/lib/structured-output.mjs @@ -221,7 +221,12 @@ export function validateJsonSchemaSafe(value, schema, path = "$", strict = false try { return validateJsonSchema(value, schema, path, strict, root); } catch (e) { - return [`${path}: schema validation aborted (${e instanceof RangeError ? "value nesting too deep" : "internal error"})`]; + // Catch ONLY the deep-nesting stack overflow (the #181 vector) and turn it into a validation + // miss → retry → refusal, never a 500. Any OTHER throw is a genuine bug: re-throw it so it + // surfaces at error level instead of being silently masked as "did not validate" (reviewer + // finding — a catch-all would hide a future TypeError behind a warn-level structured_retry). + if (e instanceof RangeError) return [`${path}: schema validation aborted (value nesting too deep)`]; + throw e; } } diff --git a/server.mjs b/server.mjs index 71c8995..7da098d 100644 --- a/server.mjs +++ b/server.mjs @@ -42,7 +42,7 @@ import { dirname, join } from "node:path"; import { homedir } from "node:os"; import { validateKey, recordUsage, getUsageByKey, getUsageTimeline, getRecentUsage, createKey, listKeys, revokeKey, closeDb, checkQuota, updateKeyQuota, getKeyQuota, findKey, cacheHash, getCachedResponse, setCachedResponse, clearCache, getCacheStats, hasCacheControl, singleflight, getInflightStats } from "./keys.mjs"; import { DEFAULT_PORT } from "./lib/constants.mjs"; -import { StructuredOutputError, detectStructuredOutput, validateJsonSchema, validateJsonSchemaSafe, extractJsonPayload, structuredSystemInstruction, resolveMaxAttempts } from "./lib/structured-output.mjs"; +import { StructuredOutputError, detectStructuredOutput, validateJsonSchemaSafe, extractJsonPayload, structuredSystemInstruction, resolveMaxAttempts } from "./lib/structured-output.mjs"; import { isLoopbackBind } from "./lib/net.mjs"; import { runTuiTurn, reapStaleTuiSessions, resolveTuiHome, bootTuiPane, tuiPaneHealthy, poolPaneName, POOL_BOOT_MS } from "./lib/tui/session.mjs"; import { detectTuiUpstreamError } from "./lib/tui/transcript.mjs"; diff --git a/test-features.mjs b/test-features.mjs index f847512..3042157 100644 --- a/test-features.mjs +++ b/test-features.mjs @@ -4405,6 +4405,12 @@ test("validateJsonSchemaSafe: well-formed value passes through unchanged (byte-i assert.deepEqual(validateJsonSchemaSafe({ name: "a" }, schema), validateJsonSchema({ name: "a" }, schema)); // error case matches too }); +test("validateJsonSchemaSafe: re-throws a non-RangeError so genuine bugs aren't masked as a validation miss", () => { + // A schema whose `required` is a non-iterable makes the inner validator throw a TypeError — that's + // a real bug, not a deep-value overflow, and must surface (not be swallowed as "did not validate"). + assert.throws(() => validateJsonSchemaSafe({ x: 1 }, { type: "object", required: 42 }), (e) => !(e instanceof RangeError)); +}); + test("validateJsonSchema: valid object passes", () => { assert.deepEqual(validateJsonSchema({ name: "a", age: 3 }, { type: "object", required: ["name", "age"], properties: { name: { type: "string" }, age: { type: "integer" } } }), []); });