fix(evaluation): Correct CSV encoding issues - #1175
Conversation
Dataset uploads decoded bytes with a hard-coded `utf-8`, so neither of Excel's two CSV exports could be uploaded: - "CSV (Comma delimited)" writes cp1252, where the en dash is the single byte 0x96. UTF-8 rejects it as an invalid start byte, returning `422 Invalid CSV file: 'utf-8' codec can't decode byte 0x96`. - "CSV UTF-8" writes a leading BOM, which survived header normalization as `question` and returned the misleading `422 Missing: ['question']`. `decode_csv_bytes` probes BOMs first (longest-first, since the UTF-32-LE BOM starts with the UTF-16-LE one), then falls back through `utf-8-sig` -> `cp1252` -> `latin-1`. cp1252 precedes latin-1 so the 0x80-0x9F range recovers the smart quotes and dashes Excel writes there instead of latin-1's C1 control characters; latin-1 is total, so the ladder cannot fall through. No new dependency. Both the v1 and v2 upload routes already funnel through `parse_csv_items`, so this covers both. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Important Review skippedAuto reviews are limited based on label configuration. 🏷️ Required labels (at least one) (1)
Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
OpenAPI changes ⚪ No API surface changesNote This PR does not modify the API contract.
|
Issue
Closes #PLEASE_TYPE_ISSUE_NUMBER
Summary
422errors for both Excel CSV formats due to encoding issues.cp1252andlatin-1to recover intended characters.Checklist
Before submitting a pull request, please ensure that you mark these task.
uv run pytest app/tests/services/evaluations/— 57 passed. Alsomypy --strictandruffclean on the touched files.TestParseCsvItemsEncodingcovering UTF-8, UTF-8+BOM, cp1252, UTF-16+BOM, UTF-32+BOM (guarding the probe order), and a byte undefined in cp1252 falling through to latin-1.parse_csv_itemsdirectly against a byte-for-byte reproduction of the failing upload (0x96 at position 292, matching the AppSignal message exactly), plus cp1252 smart quotes and a curly apostrophe. All six rows now decode to the intended characters:Notes
utf-8-sigis byte-identical toutf-8on BOM-less input, and the later rungs are only reached afterutf-8-sigraises.docs/wiki/modules/evaluations.mdper the maintenance rule.Original PR description
Issue
No linked issue — found via an AppSignal error on the Glific side (org 290):
Summary
parse_csv_itemsdecoded uploaded bytes with a hard-codedutf-8. Both of Excel's CSV export options produce bytes that fails, so there was no Excel path that worked:422 Invalid CSV file: 'utf-8' codec can't decode byte 0x96422 CSV must contain 'question' and 'answer' columns. Missing: ['question']Two independent bugs:
cp1252 — Excel autocorrects typed punctuation into smart quotes, curly apostrophes and dashes. cp1252 encodes those in
0x80–0x9F, exactly the range UTF-8 reserves for continuation bytes, so an en dash (–) is the lone byte0x96and UTF-8 rejects it as an invalid start byte. A cp1252 file containing only straight ASCII punctuation decoded fine, which is why this surfaced sporadically rather than on every Windows upload.BOM — "CSV UTF-8" prefixes
EF BB BF.field.strip().lower()does not strip, so the first header stayedquestion, failed the required-column check, and returned a misleading error naming a column the user could plainly see in their file. This also means the usual workaround advice ("re-save as CSV UTF-8") was itself broken.Fix
decode_csv_bytesinservices/evaluations/validators.py:utf-8-sig→cp1252→latin-1.utf-8-sigstrips a BOM when present and is otherwise identical toutf-8, so rung one alone kills bug 2.cp1252precedeslatin-1deliberately: latin-1 would decode0x96to U+0096, an invisible C1 control character, silently putting garbage in the dataset instead of the en dash the user typed. cp1252 recovers the intended character.latin-1is total, so the ladder cannot fall through; the trailingHTTPExceptionis there to keep the function total if that catch-all is ever removed.No new dependency —
codecsand the codecs themselves are stdlib. (chardetis present transitively inuv.lock, but an undeclared transitive is worse than a three-rung ladder that is provably correct for the encodings spreadsheets actually emit.)Both upload routes already funnel through
parse_csv_items(api/routes/evaluations/dataset.py→services/evaluations/dataset.py:88,dataset_v2.py→services/evaluations/fast.py:116), so v1 and v2 are both covered by the one change.Checklist
uv run pytest app/tests/services/evaluations/— 57 passed. Alsomypy --strictandruffclean on the touched files.TestParseCsvItemsEncodingcovering UTF-8, UTF-8+BOM, cp1252, UTF-16+BOM, UTF-32+BOM (guarding the probe order), and a byte undefined in cp1252 falling through to latin-1.parse_csv_itemsdirectly against a byte-for-byte reproduction of the failing upload (0x96 at position 292, matching the AppSignal message exactly), plus cp1252 smart quotes and a curly apostrophe. All six rows now decode to the intended characters:Notes
utf-8-sigis byte-identical toutf-8on BOM-less input, and the later rungs are only reached afterutf-8-sigraises.docs/wiki/modules/evaluations.mdper the maintenance rule.