Skip to content

An update purge deletes the children of a page that failed to fetch - #1390

Merged
xroche merged 5 commits into
masterfrom
fix-purge-unparsed-children
Aug 23, 2026
Merged

An update purge deletes the children of a page that failed to fetch#1390
xroche merged 5 commits into
masterfrom
fix-purge-unparsed-children

Conversation

@xroche

@xroche xroche commented Aug 23, 2026

Copy link
Copy Markdown
Owner

On --update the engine unlinks what the previous mirror had and this run did not write. A page that gives up after its last retry keeps its copy (#746) but is never parsed, so its links never reach new.lst, and the purge reads that gap as the site dropping them: one dropped connection on a hub page costs its whole subtree, silently, while the run reports success. An HTTP error already escapes this, being masked to a 304 and recovered from the cache. A transport failure has no such path.

The purge now holds off for the run when a link that could carry links fails to transfer, keyed on back_transfer_failed() so it cannot drift from the predicate that decides whether the copy is kept. A mangled Content-Encoding, a bad chunk length, a disk write error and an aborted external wrapper all report STATUSCODE_INVALID, keep their copy, and now keep their children with it. Two cases deliberately do not hold: an answered HTTP error, which is recovered and re-queues its links, and a page the size cap skips, whose own copy #746 drops so its children go with it rather than being stranded. The guard also requires that the previous run actually mirrored the page, since only then is there a subtree at risk. Typing the link by its savename instead let any new permanently dead .html link disable the purge on every run.

Masking the failure as a 304 was the alternative and does not work this far down the loop: that masking sits ahead of back_finalize, and doing it at the give-up point would disarm the "no data transferred, restore the previous session" rollback, so a mirror whose host had vanished would report success.

httrackp gains one field at its tail, live state copy_htsopt leaves alone. It lands in the padding after wizard_filters on LP64, so sizeof is unchanged there; on ILP32 the struct has no trailing padding and grows by four bytes, as every earlier tail field did. size_httrackp is asserted against sizeof at each entry point, so a stale-header consumer aborts rather than corrupts.

Known gap, not fixed here: a failed fetch writes no cache entry, so a hub failing on two consecutive runs has no previous entry the second time and stops holding the purge.

Test 348 drives five shapes past the guard and reads hts-changes.json rather than a log line, the two boundaries above among them. Found by the httrack-android session's overnight robustness audit, reproduced against 3.49.23.

xroche and others added 5 commits August 23, 2026 12:01
The end-of-update purge unlinks what the previous mirror had and this run did
not write. A page that fails after its last retry keeps its own copy (#746) but
is never parsed, so the links it carries are never queued and never noted: the
purge reads them as gone from the site and deletes them. A dropped connection
on one hub page therefore costs its whole subtree, unlogged at the default
verbosity, and the run still reports success.

Hold the purge back for the run when a link that could carry links gave up: the
mirror is not a complete scan of the site, so the old.lst minus new.lst
difference no longer means what the purge assumes. This is the guard an aborted
run already gets.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
A library consumer reusing one httrackp for two mirrors carried the first run's
verdict into the second and never purged again: the flag was cleared only in
hts_create_opt. Clear it where the run's other state is reset.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
Review found the guard reading the wrong evidence and missing two cases. A
content type on an error reply describes the error page, so a .bin that 500s
with an HTML body held the purge back; what the previous run cached decides
now, and this reply only when the cache has nothing. A banned host and a size
cap also stop the parser before it can queue anything, so both join the gate.
The warning moves inside the old.lst branch, where a purge could have happened.

Test 348 gains a third pass whose failures are a 404 and a lost .bin: it must
still purge. Without that, a guard that fired on any dead link at all passed
the whole suite.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
This fix had to invert an assertion in a neighbouring self-test, and its own
guard passed all 372 tests while firing on any dead link at all. Both say the
same thing, and AGENTS.md's adversarial section did not.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
The guard listed failure codes by hand and missed STATUSCODE_INVALID, which is
what a mangled Content-Encoding, a bad chunk length, a disk write error or an
aborted external wrapper all report. Those pages keep their previous copy under
#746 but never set the flag, so the purge still took every child they linked:
the bug this whole change exists to fix. Key it on back_transfer_failed()
instead, the predicate that decides whether the copy is kept, so the two sets
cannot drift apart again. banned is subsumed by it (timeout and slow are both
negative codes) and can_retry adds nothing: an answered HTTP error is recovered
from the cache on an update, which re-queues what it linked.

The other direction was worse. Typing the link fell through to its savename
when there was no cache entry, so any new permanently-dead .html link disabled
the purge for the whole run, every run. Only a page the previous run fetched
successfully has a subtree at risk, so require that instead.

The test now drives five shapes past the guard and reports through
hts-changes.json rather than a log string, including two boundaries that must
not hold: an answered 500, and a page the size cap makes the engine skip on
purpose (whose own copy #746 drops, so its children go with it).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
@xroche
xroche merged commit a513123 into master Aug 23, 2026
44 checks passed
@xroche
xroche deleted the fix-purge-unparsed-children branch August 23, 2026 18:31
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