can1357/oh-my-pi · error · anyhow::Error
Replacement text is not valid UTF-8: {err}
Error message
Replacement text is not valid UTF-8: {err} What it means
The stdout reader loop expects every non-empty line from the RPC server to be a JSON frame. A line that fails json.loads is wrapped in RpcError including the line number and a snippet (truncated to 240 chars) so you can see what the server actually printed.
Source
Thrown at crates/pi-ast/src/ops.rs:331
let mut prev_end = 0usize;
for edit in &sorted {
if edit.position < prev_end {
return Err(anyhow!(
"Overlapping replacements detected; refine pattern to avoid ambiguous edits"
));
}
prev_end = edit.position.saturating_add(edit.deleted_length);
}
let mut output = content.to_string();
for edit in sorted.into_iter().rev() {
let start = edit.position;
let end = edit.position.saturating_add(edit.deleted_length);
if end > output.len() || start > end {
return Err(anyhow!("Computed edit range is out of bounds"));
}
let replacement = std::str::from_utf8(&edit.inserted_text)
.map_err(|err| anyhow!("Replacement text is not valid UTF-8: {err}"))?;
output.replace_range(start..end, replacement);
}
Ok(output)
}
pub fn collect_matched_files(
cwd: &Path,
patterns: &[String],
) -> Result<Vec<MatchedFile>, std::io::Error> {
let globset = build_globset(patterns)?;
let mut builder = WalkBuilder::new(cwd);
builder
.hidden(false)
.git_ignore(true)
.git_global(true)
.git_exclude(true);
let mut files = Vec::new();
for entry in builder.build() {View on GitHub (pinned to 9690622007)
Solutions
- Look at the Frame snippet in the message to see the offending line — usually a log line or banner.
- Configure the server to send all logs/diagnostics to stderr, keeping stdout JSON-only.
- Remove shell wrappers/redirects (2>&1) that merge stderr into stdout.
- Ensure server and client protocol versions match (line-delimited JSON framing).
- Catch RpcError around client usage and log the snippet for diagnosis before retrying.
Example fix
// before (spawning with merged output) proc = subprocess.Popen(cmd, stderr=subprocess.STDOUT) // after proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
Defensive patterns
Strategy: try-catch
Validate before calling
# sanity-check the server binary emits line-delimited JSON before wiring the client test_proc = subprocess.run([server_cmd, "--version"], capture_output=True) assert not looks_like_plain_text(test_proc.stdout), "server prints non-JSON to stdout"
Type guard
def is_json_line(line: str) -> bool:
try:
json.loads(line); return True
except json.JSONDecodeError:
return False Try / catch
try:
result = await client.request("prompt", payload)
except RpcError as exc:
if "Failed to decode RPC output" in str(exc):
logger.error("server stdout contaminated with non-JSON; check the Frame snippet: %s", exc)
raise Prevention
- Configure the server so all logs/banners go to stderr; stdout is JSON-only.
- Never redirect stderr into stdout (2>&1) when spawning the RPC server.
- Audit env vars and wrappers that enable verbose stdout logging in the server.
When it happens
Trigger: Server printing human-readable text, warnings, debug logs, or tracebacks to stdout instead of only JSON-RPC frames; stderr leaking into stdout via shell redirection; output corruption or mixed encodings.
Common situations: Wrapping the server with a shell wrapper that echoes text, an env var enabling verbose logging that writes to stdout, an incompatible server version emitting a different framing (e.g. Content-Length headers instead of line-delimited JSON).
Understand the failure class
Background: "Invalid JSON response" and "Failed to parse response" errors: when an API answers 200 but the body isn't the JSON your library expected — this error's family across 28 libraries.
Related errors
- rpc frame must be an object
- RPC response payload must be an object
- invalid glob `{pattern}`: {error}
- V2 compaction stream parse failed: ${err instanceof Error ?
- Gemini Files API ${context} response is not valid JSON
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/7b3f5053a48484e4.
Report an issue: GitHub.