Skip to content

fix(api): batch errors no longer echo library internals — CWE-209 - #147

Merged
MrChengLen merged 2 commits into
mainfrom
pr-batch-error-messages
Sep 26, 2026
Merged

MrChengLen merged 2 commits into
mainfrom
pr-batch-error-messages

Conversation

@MrChengLen

@MrChengLen MrChengLen commented Sep 26, 2026 •

Copy link
Copy Markdown
Owner

What / why

Pre-PR security audit finding (Low, CWE-209): _do_convert_batch and _do_compress_batch returned str(e) for any ValueError. Library exceptions subclass ValueError too — UnicodeDecodeError from the strict UTF-8 reads in the Markdown/CSV/JSON converters, JSONDecodeError, some Pillow errors — so a non-UTF-8 .md in a batch put 'utf-8' codec can't decode byte 0xdc in position 2: invalid continuation byte into the per-file message, the X-FileMorph-Batch-Failures header and manifest.json.

Changes

  • app/core/batch.py — new BatchFileError, raised by both batch routes for their own messages (filename, size, magic bytes, output cap, target-size format). All existing texts unchanged.
  • app/converters/base.py — new InvalidInputError (a problem the user can fix; message written for them) and read_utf8_text(), which decodes a text upload as UTF-8 or raises InvalidInputError telling the user how to re-save it (Excel: "CSV UTF-8"; editors: Save As → Encoding). Line endings are kept, so the CSV readers parse exactly as before; a leading BOM (Excel's "CSV UTF-8" always writes one) is dropped instead of ending up in the first column name.
  • md → html, md → pdf, csv → json, csv → xlsx, json → csv use read_utf8_text(). JSON → CSV "must be a non-empty array of objects" is an InvalidInputError now.
  • Batch routes surface str(e) only for BatchFileError / InvalidInputError (convert also keeps UnsupportedConversionError). Everything else → generic "Conversion failed. Verify the file is valid." / "Compression failed. Verify the file is valid." + logger.exception (the generic text gained the next step because more files land there now).
  • Single /convert maps InvalidInputError to 400 + X-FileMorph-Error-Code: invalid_input (was a generic 500 — a dead end that API clients would also retry).
  • /api/v1/pdf/* needed no change: it already uses the dedicated PageSelectionError pattern, and _open_reader wraps pypdf errors into a caller-safe message.
  • Docs: api-reference.md (400 row), api-usage-guide.md (per-file messages), formats.md (Markdown/CSV/JSON must be UTF-8). CHANGELOG entry under [Unreleased].

Behavior changes worth knowing

  • Non-UTF-8 CSV/JSON in a batch used to show the raw decoder text; they now get the re-save hint (without the helper this fix would have turned them into a dead end).
  • Any other converter/compressor ValueError that happened to carry readable text now gets the generic message in a batch — as single /convert / /compress always did. Examples: invalid JSON syntax, the API-only pdf → pdf identity pair's "Could not read the PDF".
  • X-FileMorph-Error-Code is already in CORS expose_headers; no new route or header → network quadruple check N/A.

Commits

  1. fix(api): batch errors no longer echo library internals — CWE-209 — the fix, docs, CHANGELOG, tests.
  2. chore(test): QA fixture generator for the batch error messages — scripts/make_testdata_batch_errors.py for the manual checklist (output gitignored, only the script ships; same pattern as fix: six say-vs-do mismatches — PDF/A name, 413 hint, format lists, .htm, env #140).

Rebuilt on main 9f639ec: #148 (download-name truncation) touched convert.py / compress.py / CHANGELOG.md — the route files merge cleanly, my hunks apply unchanged on top of #148; #141 (docker.yml LF) touched only CHANGELOG.md. The CHANGELOG keeps every entry.

Review gate

commit-review: gitleaks clean, scope-guard clean, security-auditor PASS, code-reviewer approve; their W1/S1/S3–S5 suggestions are included, the rest are separate follow-ups.

Tests

  • New tests/test_conversion_error_messages.py — 15 tests (+2 WeasyPrint md → pdf variants that run in Linux CI). On main: 12 fail (leak reproduced for md/csv/json and the injected ValueErrors; BOM kept in the first CSV column), 3 pass (guards: every route-owned message still reaches the client; quoted CSV line breaks survive). With the fix: all pass.
  • Full suite (on main 9f639ec + this PR): 1265 passed, 66 skipped. ruff check/format, i18n drift-check, pip-audit (with the CI ignores) clean.

🤖 Generated with Claude Code

@MrChengLen
MrChengLen force-pushed the pr-batch-error-messages branch from 220392b to 9a7b39c Compare September 26, 2026 13:33
MrChengLen and others added 2 commits September 26, 2026 15:39
/convert/batch and /compress/batch returned str(e) for any ValueError.
Library exceptions are ValueErrors too (UnicodeDecodeError from the strict
UTF-8 reads in the Markdown/CSV/JSON converters, JSONDecodeError, some
Pillow errors), so their internals (codec, byte offset, parser position)
reached the per-file error message, the X-FileMorph-Batch-Failures header
and manifest.json.

The routes' own messages now raise BatchFileError (app/core/batch.py);
converters raise InvalidInputError (app/converters/base.py) for problems
the user can fix, with a message written for them. Only those two, plus
the existing UnsupportedConversionError, reach the client. Everything else
takes the generic branch, logged with its traceback; that message now
names a next step ("Conversion failed. Verify the file is valid."), since
more files land there. The /api/v1/pdf/* routes already work this way.

Text uploads that aren't UTF-8 — Markdown, CSV, JSON; Excel's default CSV
export on Windows is the common case — go through one helper,
read_utf8_text(), whose message tells the user how to re-save the file:
per file in a batch, and from single /convert as a 400 with
X-FileMorph-Error-Code: invalid_input instead of a generic 500 (a dead end
that API clients would also retry). Without it this fix would have turned
the batch's leaky-but-telling decoder text into a dead end. The helper
keeps line endings, so the CSV readers parse exactly as before, and drops
a leading BOM, which Excel's "CSV UTF-8" (the format the hint recommends)
always writes. The JSON -> CSV "non-empty array" hint, until now visible
only in a batch because it happened to be a ValueError, is an
InvalidInputError now.

Rejected: catching UnicodeDecodeError in the route (binary-format libraries
raise it too, where "not UTF-8 text" would mislead); decoding non-UTF-8
input leniently (no declared charset, so a cp1252 guess can be a silent
false success).

Full suite 1265 green (66 skipped; the WeasyPrint md -> pdf variants run in
CI); ruff + i18n-drift + pip-audit clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
scripts/make_testdata_batch_errors.py writes byte-stable fixtures for the
manual checklist of the batch error-message fix: a windows-1252 Markdown
file and CSV (the "not UTF-8 text" hint), a JSON object that isn't an array
(the JSON -> CSV hint) and a small JPEG for a mixed batch. Output goes to
the gitignored docs-internal/testdata/batch-errors/; only the script ships,
mirroring scripts/make_testdata_format_fixes.py.

Verified: two runs are byte-identical; each fixture gives the expected
response on /api/v1/convert and /api/v1/convert/batch; ruff clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MrChengLen
MrChengLen force-pushed the pr-batch-error-messages branch from 9a7b39c to 8c17b01 Compare September 26, 2026 13:39
@MrChengLen
MrChengLen merged commit 58e8bc2 into main Sep 26, 2026
5 checks passed
@MrChengLen
MrChengLen deleted the pr-batch-error-messages branch September 26, 2026 14:49
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