Skip to content

fix(english): report DaoTekno's in-app block and survive rate limits - #2657

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

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

Conversation

@RibatTRW

@RibatTRW RibatTRW commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Problem: DaoTekno shows "Request failed: 403" in LNReader, and its WebView shows "Access Denied". daotekno.com refuses any User-Agent containing the Android WebView ; wv token, and Cloudflare challenges every request that lacks a cf_clearance cookie. That cookie only works with the User-Agent that earned it.

Change: plugins/english/daotekno.ts (v1.0.2) no longer hardcodes a mobile-Chrome User-Agent and fake sec-ch-ua* / Sec-Fetch-* headers, so requests carry the app's own User-Agent. A Cloudflare challenge or the site's in-app "Access Denied" 403 now becomes a readable error instead of "Request failed: 403".

Verified:

  • In the plugin playground on 2026-10-11, with a cf_clearance earned by a human under a non-WebView mobile Chrome User-Agent, the plugin loads real content from daotekno.com: 50 popular novels, 18 search results for "martial", 497 chapters for Level 2 Player, and a 14,919-character chapter.
  • Without a cookie, and with a WebView cookie, the playground shows the new error messages.
  • Mock-server scenarios cover each response type (see Testing).

Not verified: the LNReader app itself (the Android toolchain isn't installed). The app's WebView still gets the site's "Access Denied" page. That is the site's choice, and this PR's code does not try to get around it.

Follow-up (v1.0.3): long novels such as "Surviving the Game as a Barbarian" failed with "Request failed: 429" because of a Cloudflare rate limit. The plugin now waits out the limit and retries. See the section near the end.

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

Recommended User-Agent for readers

The site serves readers whose User-Agent looks like Android Chrome. Readers who want to read DaoTekno in LNReader can set the app's custom User-Agent to:

Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Mobile Safari/537.36
  • Set it before opening daotekno.com in WebView to pass Cloudflare. The cf_clearance cookie only works with the User-Agent that earned it, so changing the User-Agent afterwards brings the challenge back.
  • This is the User-Agent used in the playground test below (2026-10-11): with a clearance earned under it, the plugin loaded 50 popular novels, 18 search results, 497 chapters and a 14,919-character chapter.
  • It has not been tested in the LNReader app itself.

What changed

  • plugins/english/daotekno.ts: removed the spoofed mobile-Chrome headers (User-Agent, sec-ch-ua*, Sec-Fetch-*). Requests go out with the app's own User-Agent through a single fetchHtml helper that replaces fetchSite; popularNovels, parseNovel, parseChapter and searchNovels all use it.
  • A non-OK response now throws a readable error, with the HTTP status still attached:
    • cf-mitigated: challenge: "Cloudflare challenge (HTTP 403). Open daotekno.com in WebView to pass it, then retry."
    • a 403 whose body says the in-app browser is not allowed: '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.'
    • anything else: Request failed: <status>.
  • Bumped the plugin version to 1.0.2 and updated the header comment to explain the block.

Risk: low. The change is confined to one plugin's fetch wrapper, and the version is bumped.

Why the hardcoded User-Agent was removed

Checked against the live site on 2026-10-08, by bundling the plugin and copying LNReader's fetchApi, where the app UA is a default header 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.
  • cf_clearance only works with the User-Agent that earned it. LNReader's WebView earns it under the app's own UA; the same cookie 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 it blocks in-app browsers.
  • With the app's own UA (this head): before WebView, every call shows the Cloudflare message; after WebView, every call shows the in-app-block message.
  • 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, the plugin's parsers still return 50 novels, 18 search results, 497 chapters and the chapter text (re-confirmed on 2026-10-11 below).

Testing

All screenshots are from this repo's plugin playground, not the LNReader app.

Scenario Result How Evidence
Plugin loads on the real daotekno.com ✅ pass Plugin playground, 2026-10-11, at head 6c37afb. cf_clearance earned by a human passing the Cloudflare check under a non-WebView mobile Chrome UA (Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Mobile Safari/537.36), posted to the playground proxy's /settings; the proxy forwards that UA Popular 50 novels; Search "martial" 18; Parse Novel series/level-2-player/ 497 chapters (first 1, last 499); Parse Chapter series/level-2-player/499/ 14,919 characters. Screenshots below
Real site without a cookie ✅ pass (expected error) Same playground, before the cookie was posted Search shows the Cloudflare message
Real site, WebView UA and WebView cookie ✅ pass (expected error) Playground with an Android WebView UA, 2026-10-08 Search, Parse Novel and Parse Chapter show the in-app-block message
In-app 403 "Access Denied" shows a Chrome-pointing message instead of "Request failed: 403" ✅ pass Plugin bundled and run against a local mock server Transcript below
Cloudflare challenge response gives an actionable message ✅ pass Mock server Transcript below
Unrelated 403 and 500 still report "Request failed: N" with the status attached ✅ pass Mock server Transcript below
A normal 200 catalog still parses into novels ✅ pass Mock server Transcript below
npm run check:plugin -- plugins/english/daotekno.ts ⏸️ inconclusive Live check Cloudflare HTTP 403 from the real site, the same as #2547 in CI
Plugin in the LNReader app ⏸️ not run Android toolchain not installed The app's WebView gets the site's "Access Denied" page (screenshot 1)

Screenshots

1. What the app's WebView gets from daotekno.com. 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. Real content with a human-earned Cloudflare clearance (2026-10-11). Playground, non-WebView mobile Chrome UA. Not the LNReader app.

Without the cookie, Search shows the Cloudflare message:

Playground, no cookie: Search shows the Cloudflare challenge message

With the cookie:

Playground, Search "martial": 18 novels
Playground, Popular: 50 novels
Playground, Parse Novel: Level 2 Player, 497 chapters, first chapter 1
Playground, Parse Novel: last chapters 497 to 499
Playground, Parse Chapter: chapter 499 text, 14919 characters

3. Error messages under the WebView UA (2026-10-08). The playground proxy forwards an Android WebView UA, standing in for the app's own UA. Before passing Cloudflare (no cookie):

Search: Cloudflare challenge message

After passing Cloudflare with the WebView's cookie, each call shows 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.

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();

Follow-up: long novels such as "Surviving the Game as a Barbarian" now load (v1.0.3)

Problem: some novels never opened, e.g. Surviving the Game as a Barbarian. Cloudflare rate-limits daotekno.com to about four requests per ten seconds; the fifth gets HTTP 429 (error code: 1015, Retry-After of 4 to 10 seconds). parseNovel fetches every 200-chapter list page back to back, so any novel with more than 800 chapters failed on its fifth page with "Request failed: 429". Barbarian has 939 chapters on 5 pages.

Change: fetchHtml now waits out a 429 for Retry-After + 1 second (10 s with no header), up to 4 retries. A Retry-After above 30 s fails straight away. If the limit doesn't clear, the error reads "daotekno.com is rate-limiting requests (HTTP 429). Wait a few seconds, then retry." The error keeps status 429. Version 1.0.2 → 1.0.3.

Evidence (live against daotekno.com, 2026-10-12). The cf_clearance cookie came from a headed Chromium launched with the Pixel 8 Chrome User-Agent above. The AI agent clicked the Cloudflare checkbox in that browser over the DevTools protocol, not a human. Runs used the plugin bundled with an LNReader-style fetch, and the playground with the cookie posted to its proxy:

  • A burst of 6 chapter-list requests: the first 4 return 200, the 5th and 6th return 429. The same 8 requests spaced 2.6 s apart all return 200. Barbarian's own page markup is the same as Level 2 Player's.
  • Before (6c37afb): Barbarian fails with "Request failed: 429". A 4-page novel (701 chapters) loads, but chapters opened right after it also get 429.
  • After (bundled plugin): Barbarian loads all 939 chapters in order (1 to 939). Page 5 was retried once. Chapters 1, 470 and 939 load (6,006 / 15,615 / 14,819 characters), about 14 s in total. The playground gave the same result: 939 chapters, with the proxy log showing page 5 requested twice.
  • Local mock server: a single 429 is retried and the chapters stay in order. A 429 that never clears gives the message above after 5 requests. Retry-After: 120 fails immediately.
  • How widespread: 3 of the first 20 catalog novels have more than 800 chapters, and 2 more are close to the limit. One novel, Monster Reincarnation, shows 0 chapters because the site's own page says "No chapters found." That is not a plugin problem.
  • npm run check:plugin is still INCONCLUSIVE (Cloudflare HTTP 403), as before.
  • The no-mistakes test step couldn't reach the real site (Cloudflare 403), so it only checked the 429 retry against a mock server. The live Barbarian results above come from my playground run, not from CI. Not tested in the LNReader app.

Screenshots are from the plugin playground with the cookie above, not from the LNReader app.

Before (6c37afb), Parse Novel for Barbarian:

Playground, before: Parse Novel Barbarian fails with Request failed: 429

After (v1.0.3):

Playground, Search "barbarian": 4 results including Surviving The Game As A Barbarian
Playground, Parse Novel: Surviving The Game As A Barbarian, 939 chapters
Playground, Parse Novel: chapter list ends at 939
Playground, Parse Chapter: chapter 939, 14819 characters
Playground, Parse Chapter: chapter 939 text

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

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - medium risk

✅ No issues found.

⚠️ **Test** - 2 warnings
  • ⚠️ plugins/english/daotekno.ts:37 - The real daotekno.com was unreachable (Cloudflare 403 from the runner), so the report that some novels such as 'Surviving the Game as a Barbarian' don't load could not be checked live. The 429 retry fix was only exercised against a mock server. The plugin also still doesn't send the master headers or mobile Chrome User-Agent. That conflicts with the earlier restore decision, but the author's latest commit chose otherwise and it was previously declined, so I'm only noting it.
  • ⚠️ live validation verdict: inconclusive (4 of 6 scenarios were driven live against the product); untested: Run the repo live check against the real daotekno.com, 'Surviving the Game as a Barbarian' loads on the real site
  • Live validation: ⚠️ inconclusive - 4 of 6 scenarios driven live against the product
Scenario Result Live Evidence
Run the repo live check against the real daotekno.com ⏸️ untested no Cloudflare blocks this runner. Providing a cf_clearance cookie or a network that isn't blocked would allow it.
Browse catalog, search, a 600-chapter novel (ascending order) and open a chapter ✅ pass live mock-run.txt
In-app-browser Access Denied 403 shows a clear message about opening the site in Chrome ✅ pass live mock-run.txt: popular[wv]
Cloudflare challenge, bare 403 and an oversized Retry-After 429 each report the right message ✅ pass live mock-run.txt
A 429 rate limit is waited out and the request then succeeds ✅ pass live mock-run.txt: popular[429 then ok], about 2s
'Surviving the Game as a Barbarian' loads on the real site ⏸️ untested no The real site is unreachable from this runner (Cloudflare 403). Run npm run check:plugin or open the novel in the app with network access to the site to verify.
  • npm ci
  • npm run check:plugin -- plugins/english/daotekno.ts (INCONCLUSIVE: HTTP 403 Cloudflare)
  • Bundled the real plugin with esbuild and drove all its methods against a disposable local HTTP mock of daotekno.com (script removed afterwards)
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

@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, with no actionable defects found.

Summary

Updates DaoTekno to explain Cloudflare challenges and the site's in-app browser block instead of showing only an HTTP status.

  • Routes all page requests through fetchHtml.
  • Keeps the HTTP status on thrown errors.
  • Removes the hardcoded browser headers intentionally and raises the plugin version to 1.0.2.
  • No actionable new issues were found. The real Android app was not exercised during this review.

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

… 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.
… load

daotekno.com sits behind a Cloudflare rate limit of about four requests
per ten seconds (HTTP 429, error 1015, Retry-After of a few seconds).
parseNovel fetches every 200-chapter list page back to back, so any novel
with more than 800 chapters (e.g. Surviving the Game as a Barbarian, 939
chapters over 5 pages) failed on its fifth request with "Request failed:
429". fetchHtml now waits out a 429 per Retry-After (up to 4 retries,
Retry-After capped at 30s) and reports a readable rate-limit message if
the limit persists.
@RibatTRW
RibatTRW force-pushed the fm/lnreader-daotekno-fix branch from 6c37afb to 81d278e Compare October 11, 2026 21:57
@RibatTRW RibatTRW changed the title fix(english): report daotekno's in-app browser block instead of a bare 403 fix(english): report DaoTekno's in-app block and survive rate limits Oct 11, 2026
RibatTRW added a commit to RibatTRW/lnreader-plugins that referenced this pull request Oct 11, 2026

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