The HTTP_400 "storm" — root cause
Date: 2026-08-14 · Verdict: not a storm and not an outage. 8 permanently-broken files retried ~5,800 times over five weeks, which is currently producing 98% of the entire platform's error volume.
What it actually is
| Total HTTP_400s (paddleocr) | 5,802 |
| Distinct documents | 8 (9 including one null) |
| Caller | system:paddleocr-layout (5,796 of them), surface internal |
| Window | 2026-07-15 → 2026-08-14 02:22 — still live |
| Retry spacing | avg 29 s (min 0 s) — automated, not human |
| Worst single document | 3,019 attempts over 20 days |
What the upstream says, verbatim from the stored response bodies:
{"detail":"PDF rasterization failed: Unable to get page count.
Syntax Error: Couldn't find trailer dictionary
Syntax Error: Couldn't read xref table"}
{"detail":"Image decode failed: cannot identify image file <_io.BytesIO ...>"}
Corrupt PDFs and undecodable images. These inputs can never succeed. No amount of retrying changes a missing xref table.
Why it matters: it has destroyed the error signal
Last 7 days, all error codes:
| error_code | calls | distinct docs |
|---|---|---|
| HTTP_400 | 2,555 | 9 |
| UPSTREAM_ERROR | 35 | 5 |
| HTTP_422 | 4 | 1 |
| UPSTREAM_DOWN | 1 | 1 |
98% of every error on the platform is these few files. That is what fired the early-warning alert reading "100% of last 15 min failed". The alert was not wrong — the calls really were all failing — but an alert that fires for a month without anyone acting is a broken alert. Any real incident is now invisible underneath this.
At the observed rate that is roughly 200 futile GPU/CPU OCR attempts per working hour.
Second bug found: zero-byte files are accepted
One of the looping hashes is e3b0c44298… — the SHA-256 of the empty string. It has 221 attempts.
The OCR database confirms it:
| file | size | status |
|---|---|---|
Surat Keterangan Perbaikan.pdf |
0 bytes | UPLOADED |
Surat Keterangan Perbaikan.pdf |
0 bytes | UPLOADED |
before-snapshot.json ×3 |
0 bytes | VERIFICATION_READY |
So the ingest path accepts a zero-byte upload, and something then submits it to OCR indefinitely. A 0-byte file cannot be OCR'd under any circumstances. Worse, a 0-byte .json reached VERIFICATION_READY — a state that implies a human should review extracted fields that cannot exist.
What it is not
Ruled out, so nobody re-treads it:
- Not the document pipeline. No
Documentrow has been touched since 2026-08-06, yet the 400s continued through 2026-08-14. Nothing is stuck inOCR_PROCESSING. - Not the test suite. I hashed every
.pdf/.png/.jpg/.jsonfixture onorigin/master— none matches any looping hash, and there are nocorrupt/invalid/empty-named fixtures. - Not a user hammering refresh. The caller is a system actor and spacing is machine-regular.
The trigger correlates with working hours (109 at 08:00, 77 at 09:00, 187 at 10:00, 41 at 13:00, then 8 at 02:00), so something in the internal layout path re-submits the same failed set whenever it runs.
Fixes (engine work)
- Treat HTTP_400 from an OCR upstream as permanent. Per
CONVENTIONS.md§3 only429/503 + Retry-Afterare retryable; a 4xx means the input is invalid. Record the failure with the upstream's own reason and stop. A negative cache keyed on content hash would make this structural rather than incidental. - Reject zero-byte uploads at ingest, and reject types that cannot be documents (
.json). Fail fast at the door with a clear message. - Surface the upstream's reason to the user — "PDF rusak / tidak dapat dibaca" — instead of silently retrying forever. Right now a notaris gets no signal and the platform burns GPU.
- Backfill: mark the 8 known-bad documents terminal so the loop stops today, and clean up the 0-byte rows (especially the
VERIFICATION_READYones, which are lying about being reviewable).
One observability change worth considering
The EWS alerted on failure rate, which a poison-pill trivially saturates. Alerting on distinct documents failing — or grouping by error_code + doc_hash — would have said "8 documents are permanently broken" instead of "everything is down", which is both true and actionable. Fix the cause first; this is the follow-up that stops it recurring.