Detect source charset before parsing (windows-1251 pages lose all non-ASCII text) - #7
Open
SergeBrick wants to merge 2 commits into
Open
Detect source charset before parsing (windows-1251 pages lose all non-ASCII text)#7SergeBrick wants to merge 2 commits into
SergeBrick wants to merge 2 commits into
Conversation
extract_content() hands loadHTML() an explicit '<?xml encoding="UTF-8">' declaration, so $tmpdoc->encoding always answers utf-8 afterwards. The conversion guarded by 'strtolower($tmpdoc->encoding) != "utf-8"' therefore never runs, whatever the page's real encoding is. For a windows-1251 page the cp1251 bytes are then parsed as UTF-8 and every invalid sequence is dropped, so the extracted article loses all of its Cyrillic while keeping Latin words and punctuation. That residue is non-empty, so process_article() accepts it and replaces the feed's own excerpt with it — the failure is invisible to any check based on length. Reproduces on https://infostart.ru/1c/articles/2757168/ (Content-Type: text/html; charset=windows-1251): before, extract_content() returns 8741 characters containing 0 characters in [\x{0400}-\x{04FF}]; after, 17618 characters containing 8877. Fourteen UTF-8 pages (apnic.net, ipng.ch, saotn.org, williamlam.com, woshub.com, internet-lab.ru, blog.ipspace.net) return byte-identical text before and after this change. Charset now comes from the response Content-Type, falling back to the markup's own declaration; unknown charsets are left alone because mb_convert_encoding() raises ValueError on them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The 500 KB ceiling rejects ordinary article pages: on infostart.ru four of six sampled articles weigh 528-668 KB of markup and never reach Readability, with no trace anywhere — extract_content() just returns false and the excerpt stays. Measured cost of parsing above the old ceiling, PHP 8.5, one article at a time: a 668 KB page peaks at 8.0 MiB against a memory_limit of 256M and takes 1.57s wall, of which the fetch is most. The other three sit at the same 8.0 MiB. mb_strlen() is also the wrong measure here — it is called before to_utf8(), on a payload whose encoding is still unknown, where its result is not meaningful. strlen() bounds what actually gets loaded into the DOM. Co-Authored-By: Claude Opus 5 (1M context) <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.
The bug
extract_content()passesloadHTML()an explicit<?xml encoding="UTF-8">declaration and then asks the parsed document what its encoding is:$tmpdoc->encodinganswersutf-8because of that declaration, whatever the page really is, so the conversion below it is unreachable.For a
windows-1251page the cp1251 bytes are then parsed as UTF-8 and every invalid sequence is dropped. The extracted article loses all of its Cyrillic and keeps Latin words, digits and punctuation — and that residue is non-empty, soprocess_article()'s checkaccepts it and replaces the feed's own excerpt with it. The failure is invisible to any check based on length; a reader just sees an article of punctuation.
Reproduction
https://infostart.ru/1c/articles/2757168/—Content-Type: text/html; charset=windows-1251.extract_content()length[\x{0400}-\x{04FF}]The broken decode was also halving the volume, not only destroying the script.
The change
Two commits, independent — the second is a policy change and can be dropped if you disagree with it.
Content-Type(UrlHelper::$fetch_last_content_type), falling back to the markup's own<meta charset>, and applied before parsing. Unknown charsets are left untouched, becausemb_convert_encoding()raisesValueErroron them in PHP 8. The now-dead$tmpdoc->encodingbranch is removed.memory_limit = 256Mand takes 1.57 s wall, mostly fetch.mb_strlen()was also being called before any transcoding, on a payload of unknown encoding, where its result is not meaningful.Regression check
Fourteen UTF-8 pages across seven sites — apnic.net, ipng.ch, saotn.org, williamlam.com, woshub.com, internet-lab.ru, blog.ipspace.net — return byte-identical extracted text before and after, with no exceptions raised. Six infostart.ru pages (windows-1251, four of them formerly over the ceiling) now extract with Cyrillic intact, 6141–21059 Cyrillic characters each.
Both changes have been running on a live tt-rss instance (PHP 8.5.7,
supahgreg/tt-rss:latest) against 19 feeds since 2026-08-10.🤖 Generated with Claude Code