Skip to content

Decode output incrementally, with a configurable decoder - #52

Merged
Apakottur merged 8 commits into
mainfrom
fix/utf8-incremental-decode
Jul 10, 2026
Merged

Decode output incrementally, with a configurable decoder#52
Apakottur merged 8 commits into
mainfrom
fix/utf8-incremental-decode

Conversation

@Apakottur

@Apakottur Apakottur commented Jul 10, 2026

Copy link
Copy Markdown
Owner

What?

Fix an issue with output decoding which resulted in UnicodeDecodeError in two different cases:

  1. Split multibyte char — the read loop uses non-blocking reads that grab whatever bytes sit in the pipe, with no alignment to character boundaries. A valid char like (b"\xe2\x82\xac") can arrive as b"\xe2\x82" on one read and b"\xac" on the next; each half fails to decode. Timing-dependent, so it surfaced as flaky crashes.
  2. Invalid bytes (#49) — genuinely malformed UTF-8 in the output (binary/Latin-1 data) crashed regardless of chunking.

How?

  1. Use a decoder which collects bytes incrementally and ignores invalid characters.
  2. Add initial support for custom decoders.

Command output was decoded per read with a strict `bytes.decode()`, raising
`UnicodeDecodeError` in two cases:

- A valid multibyte character split across two non-blocking reads (the read
  loop grabs whatever is in the pipe, with no alignment to char boundaries).
- Genuinely invalid UTF-8 bytes in the output (issue #49).

Decode with an incremental decoder instead: it holds a partial multibyte char
until the next read completes it, and (by default) replaces invalid bytes with
the Unicode replacement character. A fresh decoder is created per stream per
run, since incremental decoders are stateful.

The decoder is configurable via a `decoder_factory` (a zero-arg callable
returning a fresh decoder), settable on `Runner(...)` and overridable per
`run(...)` call. Defaults to UTF-8 with `errors="replace"`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Apakottur
Apakottur force-pushed the fix/utf8-incremental-decode branch from 16a3c00 to 7eb9b8d Compare July 10, 2026 13:12
@Apakottur Apakottur changed the title Decode command output incrementally to survive bad/split UTF-8 Decode output incrementally, with a configurable decoder Jul 10, 2026
Apakottur and others added 7 commits July 10, 2026 13:15
A single incremental decoder cannot decode stdout and stderr in the same run:
it buffers partial multibyte bytes, so interleaved decode() calls on the two
streams corrupt each other, and state bleeds across runs. Keep a factory so a
fresh decoder is created per stream per run.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
collections.abc.Buffer only exists on Python 3.12+, so mypy failed on the
3.10 type-check job. _typeshed.ReadableBuffer is the standard typeshed buffer
type across all versions and is only imported under TYPE_CHECKING.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Drop `typing.override` (added in 3.12; ImportError at runtime on 3.10/3.11).
- Use `_typeshed.ReadableBuffer` instead of `collections.abc.Buffer` (3.12+).
- Restore noqa for the stdlib-signature `decode` override.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Apakottur
Apakottur merged commit b3f3875 into main Jul 10, 2026
22 checks passed
@Apakottur
Apakottur deleted the fix/utf8-incremental-decode branch July 10, 2026 13:44
This was referenced Jul 10, 2026
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.

1 participant