<feed xmlns='http://www.w3.org/2005/Atom'>
<title>feedreader.git/app/src/main/java/org/terst/feedreader/readability/ExtractionFailure.kt, branch master</title>
<subtitle>FeeDreader — personal Android RSS reader
</subtitle>
<id>https://git.terst.org/feedreader.git/atom?h=master</id>
<link rel='self' href='https://git.terst.org/feedreader.git/atom?h=master'/>
<link rel='alternate' type='text/html' href='https://git.terst.org/feedreader.git/'/>
<updated>2026-10-12T00:41:24+00:00</updated>
<entry>
<title>Stop reader mode passing a site's cookie notice off as the article</title>
<updated>2026-10-12T00:41:24+00:00</updated>
<author>
<name>Peter Stone</name>
<email>thepeterstone@gmail.com</email>
</author>
<published>2026-10-12T00:41:24+00:00</published>
<link rel='alternate' type='text/html' href='https://git.terst.org/feedreader.git/commit/?id=3f988c1749460528c31dfc239a3a9544a8f52c30'/>
<id>urn:sha1:3f988c1749460528c31dfc239a3a9544a8f52c30</id>
<content type='text'>
Reported live: reader mode on a New Republic feature showed nothing but
the cookie disclaimer, and had cached it.

Reproduced outside the app by running ReaderExtractor's own injected
script (Readability.js and all) against the real page. With the article
body present, extraction is perfect. With the body absent from the DOM at
the moment the script runs, Readability's first pass -- which discards
"unlikely candidates", and the Didomi banner's class is
didomi-notice-banner -- comes back under its own 500-character threshold,
so it retries with that stripping switched off. The banner's single
paragraph is 510 characters, i.e. just over the threshold, so it comes
back as the article with the right title attached.

Nothing on the Kotlin side looked at that. Any non-blank content counted
as ExtractionResult.Success, was written to articles.extractedContent,
and was never reconsidered -- so one bad load became a permanent cookie
disclaimer, which is the "extractedContent cached: true" in the report.

ConsentNotice recognises such a result: a hard plain-text length cap
first (nothing long is ever rejected, however much consent boilerplate it
quotes), then either a known CMP container id -- Readability strips
classes but keeps ids -- or two or more distinct consent phrases.

The accept/retry/give-up decision moves out of the WebView callback into
decideEvalOutcome so it can be tested without a device. A consent notice
is now "not ready yet", which routes it into the retry the extractor
already does for an empty result; if it survives all six attempts that is
what the page serves, and it fails as ExtractionFailure.ConsentInterstitial
rather than being accepted.

A fixed extractor cannot repair what is already on disk, so
loadCachedExtraction drops a cached notice on read and re-extracts, in
both ReaderScreen and LinkReaderScreen. No schema change: the repair
happens when the article is next opened. Reader mode is still never
turned on heuristically -- replacing a discarded notice does not flip it.

The bug report now also carries the last extraction error, without which
a report about bad reader content cannot say whether extraction failed.

Tests: 59 -&gt; 78.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_018AwnALZL6bD4VBtATDVv3B
</content>
</entry>
<entry>
<title>Fix intermittent extraction failures, and stop them being silent</title>
<updated>2026-09-20T17:39:04+00:00</updated>
<author>
<name>Peter Stone</name>
<email>thepeterstone@gmail.com</email>
</author>
<published>2026-09-20T17:39:04+00:00</published>
<link rel='alternate' type='text/html' href='https://git.terst.org/feedreader.git/commit/?id=cf8ff865f7764953ded1b93c8ec42d4aff589e81'/>
<id>urn:sha1:cf8ff865f7764953ded1b93c8ec42d4aff589e81</id>
<content type='text'>
Bug report: extraction failed on one 1500days.com article while others worked.
It is not that article. Extraction was racing the page load, and the report
could say nothing more because the app discarded the reason.

Two separate defects.

1. The race. onPageFinished fires before the page has finished loading —
   observed at progress=25 on this article — and the extraction script was
   injected there. Sometimes the DOM is far enough along; sometimes
   evaluateJavascript hands back a bare "null" because the document is still
   being replaced. Measured against the reported URL: 4 successes in 6
   attempts before, 10 in 10 after.

   An unusable result is no longer final. A null or empty-content result is
   retried (6 attempts, 500ms apart, well inside the existing 25s timeout); a
   Readability error still fails immediately, since retrying will not change
   what the parser thinks of the markup.

2. The silence. ReaderScreen did `extracted?.content?.let { }`, so every
   failure — dead host, HTTP error, timeout, parser throw — arrived as the same
   null and was dropped. The reader just kept showing raw feed content. The
   only way a failure ever surfaced was the user noticing.

   extractResult() now returns a typed ExtractionFailure carrying the reason;
   ReaderScreen logs the diagnostic and shows the user a short explanation.

Also replaced a dead error callback. The only override was the four-argument
onReceivedError, which Android stopped calling at API 23; minSdk is 26, so it
never fired on any supported device. A failed load therefore invoked nothing
and hung for the full 25 seconds before surfacing as an indistinguishable null.
Now overrides the request/error and onReceivedHttpError forms Android actually
calls, both filtered to the main frame so a 404'd tracker cannot abort an
article that renders fine.

The instrumented test does NOT reproduce the race and says so in its own
docstring: a MockWebServer page finishes loading before onPageFinished fires,
and slow subresources do not help because WebView waits for them. It is kept as
a reliability guard over repeated back-to-back extractions. The race was
verified by hand against the live URL.

47 unit tests, 1 instrumented test, both green.
</content>
</entry>
</feed>
