JSON.stringify produces lone surrogates in API request body (400 invalid high surrogate)

Resolved 💬 6 comments Opened Apr 6, 2026 by tebjan Closed Jun 1, 2026

Bug

Claude Code occasionally sends API requests containing lone Unicode surrogates (U+D800–U+DFFF), causing the Anthropic API to reject them with:

API Error: 400 {"type":"error","error":{"type":"invalid_request_error",
"message":"The request body is not valid JSON: invalid high surrogate in string: line 1 column 411126 (char 411125)"}}

Root Cause

Node.js strings are internally UTF-16. When in-memory strings contain lone surrogates (from file reads, terminal output, or web content), JSON.stringify() serializes them as \uD8xx without a following low surrogate — which is invalid JSON per RFC 8259.

The stored JSONL files are clean (verified: no surrogates in the JSONL, subagent files, or tool-result cache). The lone surrogate is introduced transiently during request body construction in Node.js memory.

Common triggers on Windows

  • Emoji in code/comments read from files
  • Box-drawing characters from terminal/build output
  • Scraped web content with malformed UTF-16
  • Any Bash tool result containing non-BMP characters

Suggested Fix

Sanitize strings before JSON.stringify() when constructing the API request body. Node.js 20+ has a one-liner:

const safe = str.toWellFormed()  // replaces lone surrogates with U+FFFD

For older Node.js:

str.replace(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])/g, '\uFFFD')
   .replace(/(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g, '\uFFFD')

Impact

  • Session becomes stuck (the failed response is recorded, retrying may work but isn't guaranteed)
  • Loss of in-progress work context if user has to start a new session
  • More likely on Windows due to cp1252/UTF-16 encoding mismatches

Environment

  • Windows 11 Pro
  • Claude Code VS Code extension
  • Node.js (whatever version ships with Claude Code)
  • Python 3.14 confirmed the JSONL files themselves are valid UTF-8

Reproduction

Happens consistently when copying structured output (bullet points, chat bubbles, etc.) from another chat window into the Claude code VS extension chat field and sending it.

View original on GitHub ↗

This issue has 6 comments on GitHub. Read the full discussion on GitHub ↗