Skip to content

fix(english): report daotekno's in-app browser block instead of a bare 403 - #2657

Draft
RibatTRW wants to merge 3 commits into
lnreader:masterfrom
RibatTRW:fm/lnreader-daotekno-fix
Draft

RibatTRW wants to merge 3 commits into
lnreader:masterfrom
RibatTRW:fm/lnreader-daotekno-fix

Conversation

@RibatTRW

@RibatTRW RibatTRW commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

What Changed

  • plugins/english/daotekno.ts: removed the spoofed mobile-Chrome headers (User-Agent, sec-ch-ua*, Sec-Fetch-*). Requests now go out with the app's own User-Agent through a single fetchHtml helper that replaces fetchSite, and all call sites (popularNovels, parseNovel, parseChapter, searchNovels) use it.
  • A non-OK response now throws a readable error. A Cloudflare challenge (cf-mitigated: challenge) tells the reader to open the site in WebView and retry. A 403 whose body says the in-app browser is not allowed reports that daotekno.com blocks reading in apps and asks readers to open it in Google Chrome. Any other failure keeps Request failed: <status>. The HTTP status stays attached to the error.
  • Bumped the plugin version from 1.0.2 and updated the header comment to explain the block.

🤖 Generated with Claude Code

Supersedes #2654 (closed).

Screenshots

These come from this repo's plugin playground, not from the LNReader app. The Android toolchain isn't installed on the machine that made this change, so this fix has not been tested in the app.

How the playground was set up:

  • It ran in Chromium launched with an Android WebView User-Agent (Mozilla/5.0 (Linux; Android 14; Pixel 8 Build/AP2A.240805.005; wv) … Version/4.0 Chrome/126.0.0.0 Mobile Safari/537.36).
  • The playground proxy forwards that UA to the site, standing in for the app's own UA.
  • For the "after WebView" shots, the cf_clearance cookie came from that same browser session after it passed Cloudflare. It was posted to the proxy's /settings.

The source still does not load. That is the site's choice. These screenshots show the plugin now naming the cause instead of saying Request failed: 403.

1. What the app's WebView gets from daotekno.com. This is the site's own page, served with HTTP 403 to any UA containing ; wv. The original reporter also saw this page in the LNReader app's WebView on a real Android phone.

daotekno.com Access Denied page for an Android WebView User-Agent

2. Before passing Cloudflare (no cookie): Search shows the Cloudflare/WebView message.

Search: Cloudflare challenge message

3. After passing Cloudflare with the WebView's cookie: Search, Parse Novel and Parse Chapter each show the in-app-block message.

Search: in-app browser block message
Parse Novel: in-app browser block message
Parse Chapter: in-app browser block message

The playground's Popular tab doesn't display plugin errors; it only logs them to the console.

Why the hardcoded User-Agent was removed

This was checked against the live site on 2026-10-08. The test bundled the plugin and copied LNReader's fetchApi, where the app UA is a default header that a plugin can override.

  • The site's "Access Denied" page is triggered by the ; wv token in the User-Agent. A UA identical except for ; wv gets HTTP 200. The X-Requested-With header and the app's default fetch headers do not trigger it.
  • Cloudflare's cf_clearance cookie only works with the User-Agent that earned it. LNReader's WebView earns it under the app's own UA. The cookie paired with any other UA gets 403 cf-mitigated: challenge again.
  • With the hardcoded mobile Chrome UA (master and this PR's earlier head 1f01a83): every call (popular, search, novel, chapter) kept failing with the Cloudflare message, even with the WebView's cookie. Requests never reached the site, so readers were never told the site blocks in-app browsers.
  • With the app's own UA (this head):
    • Before WebView, every call shows "Cloudflare challenge (HTTP 403). Open daotekno.com in WebView to pass it, then retry."
    • After WebView, every call shows 'daotekno.com blocks reading in apps and in-app browsers (HTTP 403 "Access Denied") and asks readers to open the site in Google Chrome instead.'
  • Runners outside the app, such as npm run check:plugin and CI, were already blocked by Cloudflare with or without the hardcoded UA. check:plugin reports INCONCLUSIVE (HTTP 403 Cloudflare), the same result feat(english): add DaoTekno source plugin for daotekno.com #2547 got in CI.
  • The hardcoded UA made the app look like Chrome. The site explicitly refuses in-app readers, and this PR doesn't try to get around that.
  • The site's markup hasn't changed since feat(english): add DaoTekno source plugin for daotekno.com #2547. With a cookie earned under a regular non-wv mobile Chrome UA (a browser session, not the app), the plugin's parsers still return 50 novels, 18 search results, 497 chapters and the chapter text.

Related: #2547 (original plugin), #2543 (original request).

This PR was written by an AI agent (Claude), and the change and its tests were AI-driven. No human has reviewed it yet.

Risk Assessment

✅ Low: The change is confined to one plugin's fetch wrapper. It turns the 403 into an honest in-app-browser message, and the version is bumped. The header removal was already raised and declined by the user, so I did not re-raise it.

Testing

The real site was unreachable (Cloudflare 403, INCONCLUSIVE), so I bundled the actual plugin and drove it against a local mock server for each response type. The in-app block, the Cloudflare challenge, a 500, a plain 403 and a 200 catalog all behaved as intended. No screenshots were produced because the plugin has no rendered surface I could drive here. The real in-app WebView and the real site were not exercised. One warning is raised about the removed request headers.

  • Live validation: ✅ go - 4 of 5 scenarios driven live against the product
Scenario Result Live Evidence
In-app 403 'Access Denied' shows a Chrome-pointing message instead of 'Request failed: 403' ✅ pass live transcript.txt: inapp => ERROR status=403: daotekno.com blocks reading in apps and in-app browsers ... open the site in Google Chrome instead.
Cloudflare challenge response gives an actionable message ✅ pass live transcript.txt: cf => ERROR status=403: Cloudflare challenge ... Open daotekno.com in WebView
Unrelated 403 and 500 still report 'Request failed: N' with the status attached ✅ pass live transcript.txt: 500 and other403 lines
A normal 200 catalog still parses into novels ✅ pass live transcript.txt: ok => OK, one novel 'Good Novel'
Plugin loads on the real daotekno.com ⏸️ untested no The real site sits behind Cloudflare and blocks the runner. Passing it needs a cf_clearance cookie from a real browser session, which is out of reach here.
Evidence: Driver transcript
http://127.0.0.1:39713/?page=1 {
  headers: {
    Connection: 'keep-alive',
    Accept: '*/*',
    'Accept-Language': '*',
    'Sec-Fetch-Mode': 'cors',
    'Accept-Encoding': 'gzip, deflate'
  }
}
inapp => ERROR status=403: daotekno.com blocks reading in apps and in-app browsers (HTTP 403 "Access Denied") and asks readers to open the site in Google Chrome instead.
http://127.0.0.1:39713/?page=1 {
  headers: {
    Connection: 'keep-alive',
    Accept: '*/*',
    'Accept-Language': '*',
    'Sec-Fetch-Mode': 'cors',
    'Accept-Encoding': 'gzip, deflate'
  }
}
cf => ERROR status=403: Cloudflare challenge (HTTP 403). Open daotekno.com in WebView to pass it, then retry.
http://127.0.0.1:39713/?page=1 {
  headers: {
    Connection: 'keep-alive',
    Accept: '*/*',
    'Accept-Language': '*',
    'Sec-Fetch-Mode': 'cors',
    'Accept-Encoding': 'gzip, deflate'
  }
}
500 => ERROR status=500: Request failed: 500
http://127.0.0.1:39713/?page=1 {
  headers: {
    Connection: 'keep-alive',
    Accept: '*/*',
    'Accept-Language': '*',
    'Sec-Fetch-Mode': 'cors',
    'Accept-Encoding': 'gzip, deflate'
  }
}
other403 => ERROR status=403: Request failed: 403
http://127.0.0.1:39713/?page=1 {
  headers: {
    Connection: 'keep-alive',
    Accept: '*/*',
    'Accept-Language': '*',
    'Sec-Fetch-Mode': 'cors',
    'Accept-Encoding': 'gzip, deflate'
  }
}
ok => OK [{"name":"Good Novel","path":"series/x/","cover":"https://github.com/LNReader/lnreader-plugins/blob/main/icons/src/coverNotAvailable.jpg?raw=true"}]
Evidence: Driver script
import http from 'node:http';
import { build } from '~/.no-mistakes/worktrees/561a4e2e1441/01M4EXH2PPSHK5J603BT2DBFE9/node_modules/esbuild/lib/main.js';
const root = process.argv[2];
const mode = { v: 'inapp' };
const srv = http.createServer((q, r) => {
  if (mode.v === 'inapp') { r.writeHead(403, {'content-type':'text/html'}); r.end('<h1>Access Denied</h1>Sorry, reading our content via this application or in-app browser is not allowed. Please open this website directly in your google chrome mobile browser'); }
  else if (mode.v === 'cf') { r.writeHead(403, {'cf-mitigated':'challenge'}); r.end('challenge'); }
  else if (mode.v === '500') { r.writeHead(500); r.end('boom'); }
  else if (mode.v === 'other403') { r.writeHead(403); r.end('nope'); }
  else { r.writeHead(200,{'content-type':'text/html'}); r.end('<span class="btn-pagi active">1 / 3</span><a class="novel-card novel-item" href="/series/x/"><div class="novel-title">Good Novel</div></a>'); }
}).listen(0);
await new Promise(r => srv.on('listening', r));
const port = srv.address().port;
const out = '/tmp/daotekno-bundle.cjs';
await build({ entryPoints:[root+'/plugins/english/daotekno.ts'], bundle:true, platform:'node', format:'cjs', outfile:out,
  alias:{'@libs':root+'/src/libs','@':root+'/src'}, logLevel:'error' });
const mod = (await import('node:module')).createRequire(import.meta.url)(out);
const p = mod.default; p.site = `http://127.0.0.1:${port}/`;
for (const m of ['inapp','cf','500','other403','ok']) {
  mode.v = m;
  try { const n = await p.popularNovels(1); console.log(m, '=> OK', JSON.stringify(n)); }
  catch (e) { console.log(m, '=> ERROR status='+e.status+':', e.message); }
}
srv.close();
- Outcome: ⚠️ 1 warning across 1 run (38.2s)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

✅ **Review** - passed

✅ No issues found.

⚠️ **Test** - 1 warning
  • ⚠️ plugins/english/daotekno.ts:37 - The plugin no longer sends the master request headers or mobile Chrome User-Agent; it uses bare fetchApi. Review decision review-2 said to restore them exactly. The author's latest commit (6c37afb) deliberately reversed that. I did not resolve the conflict. Outside the app, requests go out with no browser User-Agent (my run showed only a minimal Accept/Sec-Fetch-Mode set), so the 'Mobile Only Content' interstitial path is untested.
  • Live validation: ✅ go - 4 of 5 scenarios driven live against the product
Scenario Result Live Evidence
In-app 403 'Access Denied' shows a Chrome-pointing message instead of 'Request failed: 403' ✅ pass live transcript.txt: inapp => ERROR status=403: daotekno.com blocks reading in apps and in-app browsers ... open the site in Google Chrome instead.
Cloudflare challenge response gives an actionable message ✅ pass live transcript.txt: cf => ERROR status=403: Cloudflare challenge ... Open daotekno.com in WebView
Unrelated 403 and 500 still report 'Request failed: N' with the status attached ✅ pass live transcript.txt: 500 and other403 lines
A normal 200 catalog still parses into novels ✅ pass live transcript.txt: ok => OK, one novel 'Good Novel'
Plugin loads on the real daotekno.com ⏸️ untested no The real site sits behind Cloudflare and blocks the runner. Passing it needs a cf_clearance cookie from a real browser session, which is out of reach here.
  • npm ci
  • npm run check:plugin -- plugins/english/daotekno.ts (INCONCLUSIVE: Cloudflare HTTP 403 from the real site)
  • node drive.mjs: bundles the real plugin with esbuild and calls popularNovels against a local HTTP server returning the in-app Access Denied 403, a cf-mitigated challenge, a 500, a plain 403 and a 200 catalog
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

… of a bare 403

daotekno.com answers HTTP 403 "Access Denied" to any User-Agent carrying
the Android WebView '; wv' token, which is LNReader's own UA, and asks
readers to open the site in Google Chrome. The plugin hid this behind a
hardcoded Chrome UA that cannot reuse the WebView's Cloudflare clearance,
so users only saw 'Request failed: 403'.

Send the app's own User-Agent and name the cause: the site's in-app
browser block, or a Cloudflare challenge that WebView can pass.
…ock is reported

The hardcoded mobile Chrome User-Agent cannot reuse the cf_clearance
cookie that LNReader's WebView earns under the app's own UA, so app
requests were re-challenged by Cloudflare and never reached the site's
in-app-browser 'Access Denied' page. Readers could only ever see the
Cloudflare message, not why the source refuses to load.

Drop the hardcoded browser headers so fetchApi sends the app's UA,
which lets the 'blocks reading in apps and in-app browsers' error fire.
Runners outside the app were already blocked by Cloudflare either way.
@RibatTRW
RibatTRW marked this pull request as draft October 8, 2026 23:30
@greptile-apps

greptile-apps Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

[Medium impact] The PR appears safe to merge for its stated goal of explaining the site's refusal rather than bypassing it.

Summary

DaoTekno now gives readers a clear explanation when the site refuses in-app browsers. It also gives separate advice for Cloudflare challenges, preserves other HTTP error messages and their status, and bumps the version to 1.0.2.

  • All requests use the shared fetchHtml helper.
  • No actionable issues were found.
  • The real site and LNReader WebView were not exercised in this review.
  • Header removal was acknowledged as intentional in the supplied PR description by RibatTRW, who stated that the user had declined restoring them. It was not raised again.

Reviews (1) · Last reviewed commit: "fix(english/daotekno): send the app's ow..." · Reviewed by Greptile

This branch has not been deployed

No deployments
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