fix: strip leading UTF-8 BOM before JSON content-shape detection - #4397
Open
chuenchen309 wants to merge 2 commits into
Open
fix: strip leading UTF-8 BOM before JSON content-shape detection#4397chuenchen309 wants to merge 2 commits into
chuenchen309 wants to merge 2 commits into
Conversation
Contributor
There was a problem hiding this comment.
All reported issues were addressed across 4 files
Shadow auto-approve: would not auto-approve because issues were found.
Re-trigger cubic
Contributor
There was a problem hiding this comment.
0 issues found across 2 files (changes from recent commits).
Shadow auto-approve: would auto-approve. Corrects UTF-8 BOM handling in file type detection with a targeted, low-risk change (strip one character) and adds a test. No core logic, infrastructure, or security impact.
Re-trigger cubic
This was referenced Jul 14, 2026
detect_filetype() decoded a file's text head as plain "utf-8", leaving
a leading byte-order-mark as a literal U+FEFF character. This defeated
_TextFileDifferentiator._is_json's content-shape check (which requires
the first non-whitespace character to be `[` or `{`), so a BOM-prefixed
JSON payload with no .json/.ndjson extension and a generic text/plain
libmagic guess fell through to FileType.TXT instead of FileType.JSON.
Strip the leading BOM once in _FileTypeDetectionContext.text_head
(consumed by _is_json/_is_csv/_is_eml) rather than at the `.encoding`
property, since an existing test asserts .encoding returns the literal
string "utf-8"/"utf-8-sig" is otherwise byte-identical to "utf-8" for
non-BOM content, so this only changes behavior when a BOM is present.
Bumped __version__.py to 0.25.1 and added a CHANGELOG.md entry per the
contribution checklist.
Disclosure: I used an AI coding assistant (Claude) to help identify
this bug and draft the fix. I independently verified the actual bug
mechanism (it required tracing through _FileTypeDetector's multi-strategy
dispatch, not just the initial hypothesis), wrote/ran the regression
test in both red and green states, ran the broader file_utils and
json/ndjson/csv/text partition suites (no regressions), and ran
ruff check/format before opening this PR.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Addresses cubic-dev-ai review feedback on this PR: `text.lstrip("")`
strips ALL leading U+FEFF characters, not just the one UTF-8 BOM that a
plain "utf-8" decode can ever produce. A second (or later) U+FEFF
immediately following the real BOM is legitimate content and should be
preserved, not silently dropped.
How verified: added a regression test constructing a file with two
leading BOM byte-sequences; confirmed it fails before the fix
(text_head == "{}" instead of "{}") and passes after. Full
test_filetype.py suite (271 passed, 1 pre-existing xfail) still green.
ruff check/format clean via `make check`.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
chuenchen309
force-pushed
the
chuenchen309/fix-json-bom-misdetection
branch
from
July 15, 2026 21:58
143723e to
5cbac58
Compare
chuenchen309
added a commit
to chuenchen309/unstructured
that referenced
this pull request
Jul 15, 2026
…letype()
_TextFileDifferentiator._is_json calls json.load() directly on the raw
file bytes when libmagic guesses a generic text/plain MIME type and the
content starts with `[`/`{`. If the bytes aren't valid UTF-8, json.load()
raises UnicodeDecodeError (via its internal detect_encoding step), which
wasn't caught by the existing `except json.JSONDecodeError`, so it
propagated straight out of the public detect_filetype() API instead of
falling back to FileType.TXT like any other undetectable content.
Broaden the except clause to also catch UnicodeDecodeError.
Added a regression test with non-UTF-8 bytes wrapped in braces and no
file extension, confirming detect_filetype() returns FileType.TXT
instead of raising. Confirmed it fails against the pre-fix code and
passes after. Ran the full test_unstructured/file_utils/ suite (380
passed, 1 xfailed -- no regressions), ruff check/format, and bumped
__version__.py + CHANGELOG.md per the contribution checklist (note:
this may create a merge conflict with my other open PR Unstructured-IO#4397, which
also bumps to 0.25.1 from the same base -- happy to rebase whichever
merges second).
Disclosure: I used an AI coding assistant (Claude) to help identify
this bug and draft the fix. I independently reproduced the
UnicodeDecodeError crash before writing the test and ran the full
local test suite before opening this PR.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
detect_filetype()decodes a file's text head as plain"utf-8". If the source has a leading UTF-8 byte-order-mark, that BOM survives the decode as a literalU+FEFFcharacter at the start of the text._TextFileDifferentiator._is_jsonrequires the first non-whitespace character to be[or{:str.lstrip()doesn't stripU+FEFF(it's not whitespace), so this check fails immediately for BOM-prefixed content —_is_jsonreturnsFalsewithout even attempting to parse it. When this happens for a payload with no.json/.ndjsonextension and libmagic guesses a generictext/plainMIME type (which it does for BOM-prefixed content — confirmed BOM changes libmagic's own guess fromapplication/json/application/x-ndjsontotext/plain), the file falls all the way through toFileType.TXTinstead ofFileType.JSON.Repro
Fix
Strip a leading BOM once in
_FileTypeDetectionContext.text_head(the property consumed by_is_json/_is_csv/_is_eml), rather than changing the.encodingproperty itself — an existing test (it_knows_the_encoding_asserted_by_the_caller_and_normalizes_it) asserts.encodingreturns the literal string"utf-8", so I kept that contract untouched and fixed the actual text consumption point instead.Per the contribution checklist, bumped
unstructured/__version__.pyto0.25.1and added aCHANGELOG.mdentry.Testing
test_it_identifies_json_with_a_leading_utf8_bom_and_no_extensiontotest_unstructured/file_utils/test_filetype.py; confirmed it fails against the pre-fix code (FileType.TXTinstead ofFileType.JSON, verified viagit stash) and passes with the fix.test_unstructured/file_utils/suite (380 passed, 1 xfailed — no regressions) plustest_json.py/test_ndjson.py/test_csv.py/test_text.py(189 passed) since those partitioners consumetext_headindirectly through detection.ruff check/ruff format --checkclean on all changed files;scripts/version-sync.sh -cconfirms the version bump is consistent.Disclosure: I used an AI coding assistant (Claude) to help identify this bug and draft the fix. The initial hypothesis about the exact failure mechanism was actually wrong (it pointed at a different function than the true cause), so I traced through
_FileTypeDetector's multi-strategy dispatch myself to find where the fix needed to land, wrote/verified the regression test in both red and green states, and ran the broader test suites above before opening this PR.