Skip to content

fix(windows): JSON API stdin decodes UTF-8 payload using legacy code page #21

Closed

Description

@HarryHeYu

Summary

util.force_utf8_stdio() reconfigures sys.stdout and sys.stderr to UTF-8, but not sys.stdin. On a Chinese-locale Windows install Python decodes stdin with the legacy ANSI code page (GBK / cp936) plus surrogateescape. Any caller that speaks the JSON-lines API over stdio and writes UTF-8 bytes therefore gets silently mis-decoded text: no exception, no warning, just a query that matches nothing.

Reported by @SunnyRad in dsh-sessionflow PR #5. This is a Core-side defect and is not part of, and not caused by, dsh-sessionflow PR #5.

Environment

  • Windows with a Chinese system locale (ANSI code page cp936 / GBK)
  • Python without PYTHONUTF8=1 and without PYTHONIOENCODING overriding stdin
  • sessionFlow Core at 1.3.1 (verified on the current main, ccfd33b)
  • A JSON-lines voyager api caller that writes UTF-8 (the DSH bridge, the VS Code client, or any MCP-style stdio client)

Reproduced in a controlled environment by forcing PYTHONIOENCODING=gbk:surrogateescape (equivalent to what a Chinese Windows console does by default). All data below is synthetic; no real user session content is involved.

Expected / Actual

transport query expected actual
voyager search 会话 --json 会话 1 hit 1 hit (correct)
voyager api stdio, {"op":"search","params":{"query":"会话"}} 会话 1 hit 0 hits, results: []
same api call, stdin forced to UTF-8 (control) 会话 1 hit 1 hit (correct)

The failing response echoes the decoded query back, which exposes the misdecode:

{"id": 1, "result": {"query": "浼氳瘽", "count": 0, "results": []}}

会话 in UTF-8 is e4 bc 9a e8 af 9d; decoding those six bytes as GBK yields 浼氳瘽. The query never matches because the wrong characters are being searched for.

With a Chinese repo value the same misdecode is worse: overview returns a silent empty list (the filter matches no garbled row), while search surfaces 'utf-8' codec can't encode character '\udcaf' ... surrogates not allowed from the surrogate-escaped bytes. In both cases 0 results / [] is indistinguishable from "there genuinely is nothing".

Root cause

voyager/util.py:31-56 — force_utf8_stdio():

for stream in (sys.stdout, sys.stderr):
    ...
    reconfigure(encoding="utf-8", errors="replace")

sys.stdin is absent from that loop. voyager/api.py:544 (serve()) reads requests with for line in sys.stdin:, so the JSON payload is decoded with the process's legacy stdin encoding before json.loads ever sees it. The fix that shipped for the stdout side (DSH bridge decoding stdout as UTF-8) covered only the output half of the same transport boundary.

Adding sys.stdin to the reconfigured streams resolves all three observations above (verified: 会话 returns 1 hit, the CJK repo empty list and the surrogate error both disappear).

Impact

  • Any UTF-8 JSON-lines api client on a non-UTF-8 Windows locale: CJK / non-ASCII query, repo, and other string params are silently corrupted.
  • Read-mostly operations fail quietly as empty results — the worst failure mode, because it looks like an empty index.
  • voyager api is the transport used by the VS Code client and stdio-based integrations; the DSH bridge is one such caller.
  • CLI argv-based commands are unaffected (Windows passes argv as UTF-16), which is exactly why voyager search 会话 --json works while the equivalent API call does not.

Acceptance criteria for a fix

Keep it minimal and at the process boundary; do not blanket-set an encoding for the whole program without checking every input path.

  • stdin, stdout and stderr are all UTF-8 at the api / CLI entry points.
  • voyager api round-trips a CJK query and a CJK repo path from UTF-8 stdin bytes.
  • Windows GBK code page: no silent misdecode, no surrogate escapes leaking into JSON.
  • JSON special characters (quotes, backslashes, embedded =, control characters) still parse.
  • Invalid UTF-8 input produces a clear error rather than a traceback or a silently empty result.
  • MCP stdio entry point (mcp_server.py, which also calls force_utf8_stdio()) stays compatible.
  • stdout/stderr behavior from the existing tests/test_utf8_stdio.py regressions is unchanged.
  • Tests must spawn a real subprocess and assert on bytes, not monkeypatch sys.stdin in-process: the bug lives in the child's locale-dependent decoding, which an in-process io.TextIOWrapper double cannot reproduce. Cover both the GBK-misdecode case and a UTF-8 control case.

Scope note

This issue is independent of dsh-sessionflow PR #5. PR #5 was a plugin-side fix to argv construction and argument validation; it neither introduced nor can fix this Core-side stdin decoding. It was found while running PR #5's integration tests against a real Core. Do not fold this into that PR.

No real session content is reproduced here — the reproduction uses a synthetic one-session index.

Activity

  1. HarryHeYu commented on Nov 11, 2026

    @HarryHeYu
    OwnerAuthor

    @SunnyRad 这个 issue 是按你在 dsh-sessionflow PR #5 里的发现建的,已标 help wanted。原始评论链接和合成索引的字节级复现都写在正文里了(e4bc9ae8af9d 按 GBK 解成 浼氳瘽、api 返回 0 行而 argv 路径命中 1 行)。

    如果你愿意接手,这个修复就交给你:force_utf8_stdio() 的 stdin 一侧、中文 query / 中文 repo 路径、stdout/stderr 与 MCP stdio 入口的回归,以及真实子进程的字节级测试。我这边不会同时提交同一修复,避免抢占你正在准备的工作;你什么时候方便就什么时候推,不需要固定时间。

    如果有更合适的做法(比如不动 force_utf8_stdio、改在 api.serve() 按字节读 stdin 再显式解码),也完全可以在 PR 里讨论。验收要点列在 issue 里。

    另外:PR #5 已 Squash Merge(b3fc6b9),作者归属保留为 SunnyRad。谢谢你的实机证据和三个修复。

  2. HarryHeYu commented on Nov 11, 2026

    @HarryHeYu
    OwnerAuthor

    补充:核对 CI 时发现 @SunnyRad 已在 PR #19(fix/utf8-stdin,87cd92a3)提交了修复,早于本 issue。本 issue 保留为缺陷与验收标准的记录,实现以 PR #19 为准,不需要重复认领或重复提 PR。

    PR #19 还额外报告了一个独立缺陷:文档与 VS Code 扩展都写 voyager api serve,但两个 parser 都不认这个动词(python -m voyager.api serve 从引入起就 exit 2),导致 VS Code 侧边栏子进程在读到第一个请求前就退出。它和 stdin 编码是两件事,Review 时应分开评估,并在 CHANGELOG / release notes 里分别列明。

    本轮未合并 PR #19(未经审查的 PR 不在授权范围内),也未由维护者并行实现同一修复。Core v1.3.1 的发布门槛仍保持阻塞,直到 PR #19 完成 Review、CI 与合并。

  3. SunnyRad commented on Nov 11, 2026

    @SunnyRad
    Contributor

    谢谢登记 issue #21,也谢谢把 dsh-sessionflow PR #5 Squash Merge(b3fc6b9)并保留署名。按你第二条评论的最新状态确认一下,方便 Review:

    不重复认领、不动 main。 实现以 PR #19(87cd92a3)为准,我不会另开 PR、不重复提修复、也不碰 main。核对过你 ae45580 / 02e0760f 两个 commit 只改了 ROADMAP 与审计报告,没有并行改代码——「维护者不并行实现同一改动」这条你确实落实了,感谢。

    一个小更正:两个缺陷其实都已修,不只是「报告」。 你写「PR #19 还额外报告了一个独立缺陷……Review 时应分开评估,并在 CHANGELOG / release notes 里分别列明」。PR #19 里 serve 动词在两个 parser(voyager/api.py::main() 与 voyager/cli.py 的 api subparser)已作为 no-op 接受,未知动词仍 exit 2;CHANGELOG.md 的 ### Fixed 下两个缺陷也已并列成两条、各自成段。所以「分别列明」这部分已落地,Review 时按两条独立改动看即可;release notes 是发布侧产物,随你那边出。

    关于实现位置,issue #21 的验收标准倾向「不动 force_utf8_stdio、改在 api.serve() 按字节读 stdin 再显式解码」。PR #19 取的是另一条路:force_utf8_stdio() 补上 sys.stdin(幂等),并在 api.serve()——三条入口唯一真正读 stdin 的位置——再调一次,这样 voyager api / python -m voyager.api / python -m voyager.api serve 全覆盖,MCP 等其它调用方也一并受益。如果你更希望按字节读 + 显式解码(能更直接满足「无效 UTF-8 明确报错」),我可以在 PR 里改,听你的。

    PR #19 对照验收标准的覆盖度(供 Review 取舍,不涉及改代码):回归测试用真实子进程、input=request.encode("utf-8")、env PYTHONIOENCODING=gbk、断言 count 而非回显,另有 test_force_utf8_covers_stdin 与 test_force_utf8_tolerates_none_stdin。已覆盖:中文 query 往返、stdin/stdout/stderr 全 UTF-8、fd 0 关闭、stdout 既有回归不变。未专门测:中文 repo 路径 / overview 空列表、无效 UTF-8 输入 → 明确报错(当前 errors="replace" 会出 U+FFFD 而非报错)、JSON 特殊字符、MCP stdio 入口显式回归。这些要不要补进 PR #19、还是合并后另开小 PR,按你的节奏,我不会在你 Review 前主动扩范围。

    CI:PR #19 分支的 tests workflow(42845178098)仍是 action_required / jobs=0(首次外部贡献者待批准);main 上 42961042487 的 success 是文档 commit 的,与 PR 分支无关。分支上的独立证据要等你批准 workflow 后才有。本机全量 1376 passed, 7 skipped, 2 failed 仅作旁证,2 个失败是 test_scan_streaming.py 硬编码 E:/code/voyager(已在 issue #20 另记,pristine main 上同样复现),与 PR 代码无关。

    等你的 Review 结论,随时配合。

  4. HarryHeYu commented on Nov 11, 2026

    @HarryHeYu
    OwnerAuthor

    Fixed by Core PR #19 — fix(util): decode stdin as UTF-8, not the ANSI code page — which adds sys.stdin to force_utf8_stdio() so the JSON-lines api transport decodes UTF-8 payloads correctly on non-UTF-8 Windows locales (GBK / cp936). With the fix, a UTF-8 query/repo over stdin round-trips instead of being silently mis-decoded into a zero-result search.

    Thanks to @SunnyRad for the original real-world report against dsh-sessionflow PR #5 — that thread is what surfaced this Core-side stdin gap. Closing as resolved in v1.3.1.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workinghelp wantedExtra attention is needed

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions