Skip to content

Harden JSONL parsing against invalid UTF-8 and non-object rows #6

Description

@VirtualPaul

Summary

A malformed JSONL file can terminate recomputation or the watcher. Invalid UTF-8 escapes load() as UnicodeDecodeError, and valid JSON scalar/array lines are accepted even though all downstream code expects dictionaries.

Tested against main at fb265549480ff0d3ad8b14b28007370539b70cf3.

Reproduction

from pathlib import Path
import tempfile, meter

root = Path(tempfile.mkdtemp())

bad_utf8 = root / "bad-utf8.jsonl"
bad_utf8.write_bytes(b'{"type":"user"}\n\xff\n')
meter.load(str(bad_utf8))  # UnicodeDecodeError

non_objects = root / "non-objects.jsonl"
non_objects.write_text('[]\n42\n{"type":"user"}\n', encoding="utf-8")
rows = meter.load(str(non_objects))  # returns list, int, dict
# recompute() later calls .get() and raises AttributeError on the list/int

Truncated JSON and ordinary invalid JSON are skipped successfully; these two cases are not.

Root cause

meter.py:293-307 reads the entire file as strict UTF-8, only catches FileNotFoundError around I/O, and appends any value accepted by json.loads() without checking its type.

Suggested fix

  1. Stream the file line by line rather than reading the full log into memory.
  2. Handle decode failures per line (for example, open with errors="replace" and skip lines that no longer parse) and catch relevant OSError/UnicodeError failures.
  3. Append only JSON objects: if isinstance(value, dict): ....
  4. Record a bounded warning/counter for skipped rows so corruption is diagnosable without flooding logs.
  5. Preserve tolerance for a partially written final line.

Regression tests

  • Valid row, invalid UTF-8, valid row: both valid dictionaries survive.
  • Valid JSON array/string/number/null: all are skipped.
  • Truncated final line and arbitrary garbage: no exception.
  • Permission/read OSError: watcher remains alive and reports degraded state.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions