can1357/oh-my-pi · error · SyntaxError
Unexpected end of JSON input
Error message
Unexpected end of JSON input
What it means
RelaxedJson.parse in strict (non-partial) mode throws SyntaxError('Unexpected end of JSON input') when the input, after whitespace/comments, contains nothing to parse — i.e. an empty or whitespace/comment-only string. Unlike partial mode (which returns undefined for streaming), strict mode refuses to silently accept an empty document, since a final parse must not accept a half-formed tool call.
Source
Thrown at packages/utils/src/json-parse.ts:198
* final parse never silently accepts a half-formed tool call.
*/
class RelaxedJson {
readonly #s: string;
readonly #n: number;
readonly #partial: boolean;
#i = 0;
constructor(source: string, partial: boolean) {
this.#s = source;
this.#n = source.length;
this.#partial = partial;
}
parse(): unknown {
this.#ws();
if (this.#i >= this.#n) {
if (this.#partial) return undefined;
throw new SyntaxError("Unexpected end of JSON input");
}
const value = this.#value(false);
if (value === INCOMPLETE) return undefined;
this.#ws();
if (!this.#partial && this.#i < this.#n) {
throw new SyntaxError(`Unexpected trailing characters at position ${this.#i}`);
}
return value;
}
#ws(): void {
const s = this.#s;
for (;;) {
while (this.#i < this.#n && isWhitespace(s.charCodeAt(this.#i))) this.#i++;
if (this.#i + 1 < this.#n && s.charCodeAt(this.#i) === 0x2f /* / */) {
const next = s.charCodeAt(this.#i + 1);
if (next === 0x2f /* / line comment */) {
this.#i += 2;View on GitHub (pinned to 9690622007)
Solutions
- Check for empty/whitespace-only input before strict parsing and handle it explicitly
- Use partial mode (the partial flag/API) when parsing streaming/incomplete input
- Fix the upstream producer — the stream or file should not deliver an empty payload to a final parse
- Catch SyntaxError and treat as malformed input with a fallback
Example fix
// before
const value = parseJson(chunk); // throws on empty chunk
// after
const value = chunk.trim().length === 0 ? undefined : parseJson(chunk);
// or for streams: parseJson(chunk, { partial: true }) Defensive patterns
Strategy: type-guard
Validate before calling
function isParsableInput(s: string): boolean {
return s.trim().length > 0;
}
if (!isParsableInput(raw)) return undefined; // skip strict parse of empty input Try / catch
let value: unknown;
try {
value = parseJson(raw); // strict
} catch (err) {
if (err instanceof SyntaxError && err.message === "Unexpected end of JSON input") {
value = undefined; // empty stream chunk — handle as 'no data'
} else {
throw err;
}
} Prevention
- Check for empty/whitespace-only input before strict parsing
- Use partial mode for streaming or possibly-truncated input
- Validate upstream streams/files actually delivered bytes before final parse
- Distinguish 'empty input' from 'malformed input' — they throw different SyntaxErrors in this parser
When it happens
Trigger: Calling parseJson (strict) with an empty string, a string of only whitespace, or input consisting solely of // or /* */ comments — this.#i >= this.#n after #ws() with #partial false.
Common situations: Parsing an empty tool-call argument buffer from an LLM stream that got cut off before any bytes arrived; reading a truncated/empty file and feeding it to the relaxed parser; splitting a stream into chunks and parsing an empty trailing chunk in strict mode.
Understand the failure class
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- Unexpected token at position ${this.#i}
- Replacement text is not valid UTF-8: {err}
- V2 compaction stream parse failed: ${err instanceof Error ?
- Gemini Files API ${context} response is not valid JSON
- No JSON payload found in response
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/e30224a973270780.
Report an issue: GitHub.