Skip to content

fix(tail): fall back to raw line on malformed JSON instead of aborting - #312

Open
detail-app[bot] wants to merge 1 commit into
masterfrom
detail/bug-fix/fix-tail-fall-back-to-raw-line-on-malformed-json-i-0a09b2
Open

detail-app[bot] wants to merge 1 commit into
masterfrom
detail/bug-fix/fix-tail-fall-back-to-raw-line-on-malformed-json-i-0a09b2

Conversation

@detail-app

@detail-app detail-app Bot commented Sep 16, 2026

Copy link
Copy Markdown

Detail bug report: View on Detail

Fixes ENG-713

Bug

When edge-tail runs with both -f json and --policy, a single syntactically invalid JSON line aborts the entire run with a non-zero exit (error.SyntaxError / error.UnexpectedEndOfInput). The malformed line and all lines after it are dropped; the run stops at the bad line.

Root cause: parseJsonAttrs in src/tail/eval_parse.zig used try std.json.parseFromSliceLeaky(...), so a malformed line's parse error propagated out of parseLineevalLineResultevalLine → the framer → runStream/runFilesLoopmain, killing the process. This was inconsistent with the existing handling of valid-but-non-object JSON values, which already fell back to the raw line via if (parsed != .object) return;.

This only triggers when an operator opts into both -f json and --policy (the shipped defaults are .raw / no policy, where parsing is never reached).

Fix

Scope error handling in parseJsonAttrs so per-line content/structure errors fall back to the raw line while error.OutOfMemory still propagates:

const parsed = std.json.parseFromSliceLeaky(std.json.Value, ctx.allocator, line, .{}) catch |err| switch (err) {
    error.OutOfMemory => return err,
    else => return,
};

This makes a malformed JSON line class with the already-non-fatal valid-but-non-object case (and the framer's oversized-line fail-open behavior): the line is kept verbatim as its own message/body and the run continues. error.OutOfMemory stays fatal-to-parseLine (resource exhaustion is a machinery failure, not a per-line content problem), as does OOM from the post-parse dupe/append calls which still use bare try. A blanket catch {} was avoided so genuine memory pressure is never silently swallowed.

The fix is intentionally in parseJsonAttrs, not parseLineparseLine's existing if (ctx.message == null) ctx.message = line; fallback then becomes reachable on the error path.

Testing

  • Unit tests, lint (task lint = zig fmt --check + ziglint), and build all pass. zig build test is green (522 pass, 1 pre-existing skip, 0 fail).
  • Added 3 regression tests:
    • eval parse: malformed json falls back to raw line — covers the SyntaxError path.
    • eval parse: truncated json falls back to raw line — covers UnexpectedEndOfInput (a realistic trigger: producer killed mid-write, log rotation mid-line), and guards against narrowing the catch to only SyntaxError.
    • eval stream public API: malformed json line does not abort evaluation — covers the integration path: a keep/drop policy still applies to the malformed line's raw body, and a subsequent valid JSON line is still parsed and evaluated correctly.
  • Negative confirmation: reverting the one-line fix (keeping the tests) makes the two eval parse tests fail with SyntaxError/UnexpectedEndOfInput and the eval stream test fail on the malformed line — proving the tests genuinely guard the bug.
  • End-to-end via the real edge-tail binary (zig build tail), input {"message":"first"}\n{not valid json}\n{"message":"third"}\n with a keep-all policy:
    • Before the fix: exit 1, error: SyntaxError on stderr, only {"message":"first"} persisted — reproduces the report.
    • After the fix: exit 0, empty stderr, all three lines preserved verbatim (malformed line included).
    • Regression checks: -f raw --policy (no JSON parsing) and -f json without --policy (disabled evaluator) both still exit 0 with all three lines, confirming the bug remains gated on both flags.
    • File-tail path (--read-from head -f json --policy): the fixed follower stays alive and emits all three lines; the buggy follower self-exits on the malformed line.
  • OOM propagation verified empirically with std.testing.FailingAllocator: error.OutOfMemory propagates from both the parse call (the error.OutOfMemory => return err arm) and the post-parse dupe/append (bare try), so the scoped catch does not silently drop attribute enrichment under memory pressure.
  • The Python integration suite (bench/logging) was run where uv was installed during testing. The -f json --policy happy-path regression guard (test_policy_json_attribute_drop) and all other stdin-based tail tests pass. A few file-tail/chmod-based tests fail or hang in this sandbox, but those are pre-existing/environmental (confirmed by re-running them on the baseline with the fix stashed — they fail identically; the chmod(0) failure-injection tests hang because the environment runs as root, which bypasses chmod(0)) and none of them exercise the -f json --policy path this fix touches.

Automatic Fixes PRs can be configured here.

@macroscopeapp

macroscopeapp Bot commented Sep 16, 2026

Copy link
Copy Markdown

Approvability

Verdict: Approved at f663e5b

Macroscope's review found this PR approvable — Malformed JSON is now handled as raw line data in the explicitly enabled JSON-policy tail path instead of terminating the stream, while out-of-memory errors remain fatal. The implementation is isolated to parsing with focused regression coverage; the second changed file contains tests only.

You can add or adjust custom eligibility rules. Learn more.

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

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant