Skip to content

Conversation

@coleleavitt
Copy link
Contributor

@coleleavitt coleleavitt commented Jan 12, 2026

Fixes #7889
Fixes #7715

Summary

Fixed "Unexpected end of JSON input" and "Unterminated string" errors in storage and SDK client.

1. SDK Client (both v1 and v2 client.gen.ts)

  • Handle empty responses with proper JSON parsing check
  • Added try-catch with contextual error messages for malformed JSON

2. Storage Layer - Empty Files (storage.ts - commit 23848ed)

  • Handle empty JSON files gracefully in read() and update() functions
  • Check for empty files before parsing, throw NotFoundError instead of JSON parse error

3. Storage Layer - Corrupted Files (storage.ts - commit 061652e)

  • NEW: Detect null bytes and control characters in JSON files
  • NEW: Comprehensive JSON parse error handling with descriptive messages
  • NEW: Make stats command resilient to corrupted files

Root Cause (Corrupted Files)

Found 3 production storage files containing 1518 null bytes each instead of valid JSON:

  • .trim() check only catches whitespace, not null bytes
  • JSON.parse() crashed with "Unterminated string" error
  • stats command failed because Promise.all() had no error handling

Changes

SDK Client

// Read response as text first, only parse JSON if non-empty
const text = await response.text()
if (!text) return undefined
data = JSON.parse(text)

Storage - Empty File Check

const content = await Bun.file(target).text()
if (!content.trim()) {
  throw new NotFoundError({ message: `Empty file: ${target}` })
}

Storage - Corruption Detection (NEW)

// Detect null bytes and control characters
const hasControlCharacters = /[\x00-\x08\x0B\x0C\x0E-\x1F]/.test(content)
if (hasControlCharacters) {
  throw new NotFoundError({ message: `Corrupted file detected: ${target}` })
}

// Wrap JSON.parse with error handling
try {
  const result = JSON.parse(content)
  return result as T
} catch (e) {
  const message = e instanceof Error ? e.message : String(e)
  throw new NotFoundError({ message: `Failed to parse JSON from ${target}: ${message}` })
}

Stats Command Resilience (NEW)

// Skip corrupted files gracefully instead of crashing
const projects = await Promise.all(
  projectKeys.map((key) => Storage.read<Project.Info>(key).catch(() => undefined))
)

Testing

  • ✅ Detected and handled 3 corrupted files in production storage
  • opencode-dev stats works (processed 4,773 sessions successfully)
  • ✅ All type checks pass
  • ✅ Empty file handling works as expected

Files Changed

  • packages/opencode/src/storage/storage.ts - Empty + corrupted file handling
  • packages/opencode/src/cli/cmd/stats.ts - Resilient error handling
  • SDK client files (auto-generated) - Empty response handling

@github-actions
Copy link
Contributor

Thanks for your contribution!

This PR doesn't have a linked issue. All PRs must reference an existing issue.

Please:

  1. Open an issue describing the bug/feature (if one doesn't exist)
  2. Add Fixes #<number> or Closes #<number> to this PR description

See CONTRIBUTING.md for details.

@github-actions
Copy link
Contributor

The following comment was made by an LLM, it may be inaccurate:

Potential Duplicate Found:

PR #7618: fix(sdk): improve JSON parsing error handling with contextual messages
#7618

Why it's related: This PR also addresses JSON parsing errors in the SDK. Both PRs are fixing issues with JSON parsing in the SDK client, though PR #7618 appears to focus on error handling with contextual messages while PR #7888 (the current PR) specifically handles empty response bodies. These may be tackling related or overlapping issues in the same codebase area.

Fixes 'Unexpected end of JSON input' error when server returns empty body.

Root cause: OPTIONS handler at /zen/v1/models returned status 200 with null body,
but SDK only handled empty responses for status 204 or Content-Length: 0.

Changes:
- Server: OPTIONS handler now returns 204 (No Content) instead of 200
- SDK (v1 & v2): JSON parsing now reads text first and handles empty bodies
  by returning {} instead of failing on response.json()
@coleleavitt coleleavitt force-pushed the fix/json-parse-empty-response branch from 2ed9637 to 2b848ef Compare January 12, 2026 00:48
@coleleavitt
Copy link
Contributor Author

Created issue #7889 to track this bug. PR description updated with Fixes #7889.

After Anthropic OAuth login, sync.data.agent is empty while data loads.
This caused crashes when accessing local.agent.current().name before
agents were populated.

Changes:
- Make agentStore.current nullable with optional chaining on init
- Add fallback to first agent in current() when not found
- Add early return guards in submit(), cycle(), cycleFavorite(), set()
- Add null checks in highlight(), spinnerDef() memos
- Use optional chaining in JSX for agent name display
@mrlubos
Copy link
Contributor

mrlubos commented Jan 13, 2026

@coleleavitt the client crash will be fixed upstream hey-api/openapi-ts#3202

- Check for empty files before parsing to avoid 'Unexpected end of JSON input' error
- Throw NotFoundError instead of JSON parse error for empty files
- Apply fix to both read() and update() functions
@coleleavitt coleleavitt changed the title fix(sdk): handle empty JSON response bodies gracefully fix(storage): handle empty JSON files gracefully Jan 14, 2026
- Add null byte/control character detection in storage read/update
- Wrap JSON.parse in try-catch with descriptive error messages
- Make stats command resilient to corrupted files by catching errors
- Fixes 'JSON Parse error: Unterminated string' crashes in stats command
@coleleavitt
Copy link
Contributor Author

Additional Fix: Corrupted File Detection

Added comprehensive error handling for corrupted JSON files containing null bytes or control characters.

New Changes (commit 061652e)

Problem Found:

  • Found 3 storage files with 1518 null bytes each (instead of valid JSON)
  • .trim() check only catches whitespace, not null bytes
  • JSON.parse() failed with "Unterminated string" error
  • stats command crashed because Promise.all() had no error handling

Solution:

  1. Corruption Detection - Added null byte/control character check in storage.ts:

    const hasControlCharacters = /[\x00-\x08\x0B\x0C\x0E-\x1F]/.test(content)
    if (hasControlCharacters) {
      throw new NotFoundError({ message: `Corrupted file detected: ${target}` })
    }
  2. JSON Parse Error Handling - Wrapped JSON.parse() in try-catch with descriptive errors

  3. Resilient Stats Command - Made stats.ts skip corrupted files gracefully:

    Storage.read<Project.Info>(key).catch(() => undefined)

Testing:

  • ✅ Detected and handled 3 corrupted files in production storage
  • opencode-dev stats now works (processed 4,773 sessions successfully)
  • ✅ All type checks pass

This prevents the "JSON Parse error: Unterminated string" crash that occurred with corrupted storage files.

@mrlubos
Copy link
Contributor

mrlubos commented Jan 15, 2026

@coleleavitt do you think this check also belongs upstream?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SDK throws 'Unexpected end of JSON input' on empty response bodies Unexpected end of json

2 participants