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

  1. Check for empty/whitespace-only input before strict parsing and handle it explicitly
  2. Use partial mode (the partial flag/API) when parsing streaming/incomplete input
  3. Fix the upstream producer — the stream or file should not deliver an empty payload to a final parse
  4. 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

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

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/e30224a973270780. Report an issue: GitHub.