{"record":{"id":"27c614215c130fb4","repo":"can1357/oh-my-pi","slug":"unexpected-trailing-characters-at-position-this","errorCode":null,"errorMessage":"Unexpected trailing characters at position ${this.#i}","messagePattern":"Unexpected trailing characters at position (.+?)","errorType":"exception","errorClass":"SyntaxError","httpStatus":null,"severity":"error","filePath":"packages/utils/src/json-parse.ts","lineNumber":204,"sourceCode":"\t#i = 0;\n\n\tconstructor(source: string, partial: boolean) {\n\t\tthis.#s = source;\n\t\tthis.#n = source.length;\n\t\tthis.#partial = partial;\n\t}\n\n\tparse(): unknown {\n\t\tthis.#ws();\n\t\tif (this.#i >= this.#n) {\n\t\t\tif (this.#partial) return undefined;\n\t\t\tthrow new SyntaxError(\"Unexpected end of JSON input\");\n\t\t}\n\t\tconst value = this.#value(false);\n\t\tif (value === INCOMPLETE) return undefined;\n\t\tthis.#ws();\n\t\tif (!this.#partial && this.#i < this.#n) {\n\t\t\tthrow new SyntaxError(`Unexpected trailing characters at position ${this.#i}`);\n\t\t}\n\t\treturn value;\n\t}\n\n\t#ws(): void {\n\t\tconst s = this.#s;\n\t\tfor (;;) {\n\t\t\twhile (this.#i < this.#n && isWhitespace(s.charCodeAt(this.#i))) this.#i++;\n\t\t\tif (this.#i + 1 < this.#n && s.charCodeAt(this.#i) === 0x2f /* / */) {\n\t\t\t\tconst next = s.charCodeAt(this.#i + 1);\n\t\t\t\tif (next === 0x2f /* / line comment */) {\n\t\t\t\t\tthis.#i += 2;\n\t\t\t\t\twhile (this.#i < this.#n && s.charCodeAt(this.#i) !== 0x0a) this.#i++;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif (next === 0x2a /* * block comment */) {\n\t\t\t\t\tthis.#i += 2;\n\t\t\t\t\twhile (","sourceCodeStart":186,"sourceCodeEnd":222,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/utils/src/json-parse.ts#L186-L222","documentation":"This error is thrown by RelaxedJson.parse (strict mode) when, after successfully parsing one complete JSON value, non-whitespace characters remain at the end of the input. The relaxed parser tolerates many LLM malformations, but it deliberately refuses trailing garbage in strict mode so a final parse never silently accepts a malformed, doubled, or half-formed document.","triggerScenarios":"Calling parseJsonWithRepair(json) (strict mode) with input containing extra content after the first value, e.g. two concatenated objects '{\"a\":1}{\"b\":2}', a stray '}' or prose after the value, or a truncated suffix like '{\"a\":1}]'. Also fired when a position is reported, so this.#i names the byte offset where the trailing characters start.","commonSituations":"Concatenating multiple LLM tool-call argument chunks without checking whether the first was already a complete value; a provider streaming two sibling tool calls into one buffer; log or template output with debug text appended after the JSON payload.","solutions":["Trim and inspect the input at the reported position to find the stray characters","Split concatenated JSON values (e.g. NDJSON / sibling tool-call buffers) and parse only the first value, or parse each separately","If the caller wants prefix-tolerant behavior for incomplete input, use parseStreamingJson instead of parseJsonWithRepair","Use classifyJsonPrefix to detect a second top-level value or trailing garbage before parsing"],"exampleFix":"// before: two values glued together\nconst v = parseJsonWithRepair<{a:number}>(`{\"a\":1}{\"b\":2}`); // throws\n// after: parse the first value only\nconst text = `{\"a\":1}{\"b\":2}`;\nconst v = parseJsonWithRepair<{a:number}>(text.slice(0, 7)); // {a: 1}","handlingStrategy":"try-catch","validationCode":"import { classifyJsonPrefix } from \"@oh-my-pi/pi-utils/json-parse\";\nif (classifyJsonPrefix(text) === \"complete\") {\n  // safe to final-parse\n}","typeGuard":"function isCompleteSingleValue(text: string): boolean {\n  try { JSON.parse(text); return true; } catch { return false; }\n}","tryCatchPattern":"try {\n  const value = parseJsonWithRepair<T>(text);\n} catch (err) {\n  if (err instanceof SyntaxError && err.message.startsWith(\"Unexpected trailing characters\")) {\n    // split concatenated values or slice to the first complete value\n  } else throw err;\n}","preventionTips":["Parse each JSON value separately instead of concatenating buffers","Use classifyJsonPrefix before final-parsing streamed buffers","Check the reported position in the message to pinpoint the stray characters"],"tags":["json","parse","trailing-characters","syntaxerror"],"backgroundTag":"json-trailing-characters","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}