iOfficeAI/OfficeCLI · error · CliException

protocol_mismatch

protocol_mismatch

Error message

Format-handler plugin '{_plugin.Manifest.Name}' wrote non-JSON to stdout (a JSONL envelope was expected): {ex.Message}. First chars: "{Truncate(line, 80)}". This is a plugin bug — diagnostic output must go to stderr or --log-file, not stdout.

What it means

Thrown when the plugin's stdout reply line is not valid JSON — System.Text.Json's JsonNode.Parse throws a JsonException. The raw parser message (e.g. "'d' is an invalid start of a value") is opaque, so this wrapper names the plugin, shows the first 80 characters of what was actually written, and explains that stdout is a JSONL-only channel. The session is marked _broken because a protocol-shape failure poisons the framing contract.

Source

Thrown at src/officecli/Core/Plugins/FormatHandlerSession.cs:235

                // Any protocol-shape failure poisons the session: §6.7 lists
                // "malformed reply" as a broken-state trigger, so we mark
                // _broken before throwing so the next Send fast-fails instead
                // of trying to write into a session whose protocol invariants
                // are gone. We also catch JsonException explicitly: the raw
                // System.Text.Json message ("'d' is an invalid start of a
                // value...") is opaque to users — wrap it in a clear
                // `protocol_mismatch` envelope that names the plugin and
                // shows a preview of what it actually wrote.
                JsonObject? reply;
                try
                {
                    reply = JsonNode.Parse(line)?.AsObject();
                }
                catch (JsonException ex)
                {
                    _broken = true;
                    throw new CliException(
                        $"Format-handler plugin '{_plugin.Manifest.Name}' wrote non-JSON to stdout (a JSONL envelope was expected): {ex.Message}. " +
                        $"First chars: \"{Truncate(line, 80)}\". This is a plugin bug — diagnostic output must go to stderr or --log-file, not stdout.")
                    { Code = "protocol_mismatch" };
                }

                if (reply is null)
                {
                    _broken = true;
                    throw new CliException(
                        $"Format-handler plugin '{_plugin.Manifest.Name}' reply is not a JSON object. First chars: \"{Truncate(line, 80)}\".")
                    { Code = "protocol_mismatch" };
                }

                var replyType = reply["msg_type"]?.GetValue<string>() ?? "";
                if (replyType == "ok")
                    return reply["result"];
                if (replyType == "error")
                {

View on GitHub (pinned to 1ced45e900)

Solutions

  1. Inspect the 'First chars' preview in the error message to identify exactly what was written to stdout.
  2. Redirect the plugin's logging framework to stderr: Python `logging.basicConfig(stream=sys.stderr)`, Node `console.error(...)`, etc.
  3. Add a `--log-file` flag to the plugin so verbose output goes to a file, not stdout/stderr.
  4. Test the plugin in isolation: pipe its stdout through `jq .` to confirm every line is a valid JSON object.

Example fix

// before: Python plugin logs to stdout by default
import logging
logging.basicConfig(level=logging.DEBUG)  # defaults to stderr, but...
# ...or worse:
print(f"Processing slide {n}")  # goes to stdout, breaks framing

// after
import logging, sys
logging.basicConfig(level=logging.DEBUG, stream=sys.stderr)
sys.stderr.write(f"Processing slide {n}\n")
Defensive patterns

Strategy: try-catch

Try / catch

try
{
    var result = session.Send(msgType, command, args, props);
}
catch (CliException ex) when (ex.Code == "protocol_mismatch")
{
    // The 'First chars' preview in ex.Message shows what the plugin wrote to stdout.
    // The session is broken — respawn it after fixing the plugin's output routing.
    session.Dispose();
    throw;  // or respawn + retry after plugin fix
}

Prevention

When it happens

Trigger: FormatHandlerSession.SendRaw reads a line that starts with a non-JSON character or contains malformed JSON. JsonNode.Parse(line) throws inside the try block at line 228-231. Typical cause: the plugin printed a stack trace, a progress message, a banner, or a bare string to stdout.

Common situations: Plugin uses a logging library (Python logging, log4j, serde) configured with a StreamHandler/ConsoleAppender on stdout by default. Plugin prints a version banner or 'Starting...' line on startup. Plugin's framework (e.g. argparse, click) echoes to stdout instead of stderr. A crash traceback lands on stdout before the reply.

Related errors


AI-assisted analysis of iOfficeAI/OfficeCLI@1ced45e900 (2026-08-13). Data as JSON: /api/errors/606616e625834b4e. Report an issue: GitHub.