Skip to content

M1: obey robots.txt on every request, add --no-robots - #18

Merged
tamnd merged 2 commits into
mainfrom
feat/robots
Aug 15, 2026
Merged

tamnd merged 2 commits into
mainfrom
feat/robots

Conversation

@tamnd

@tamnd tamnd commented Aug 15, 2026

Copy link
Copy Markdown
Owner

Closes #9. Part of #8.

What was wrong

v0.2.0 fetched /search, /review/list and /review/list_rss.
All three are disallowed by goodreads robots.txt and the tool never looked.

What this does

Adds the check and puts it in Client.Fetch rather than Client.Do.
Do would only cover call sites ported to the new op registry, and the defect was precisely that call sites built their own URLs.
A check you can skip by not calling the right function is not a check.

goodread/robots.go parses the User-agent: * group with longest match wins and Allow beating Disallow at equal length.
That precedence is load bearing: Allow: /work/editions is longer than Disallow: /work, so editions and quotes stay readable and the bare work page does not.
Matching runs on path plus query, because Disallow: /*reviewFilters can only ever match a query string.
Rules are fetched on first use, cached an hour, and a fetch failure is exit 8 with nothing attempted.
There is no compiled in fallback copy, because a stale copy that says yes is worse than no answer.

goodread/surfaces.go registers every path the tool can read.
TestNoDisallowedByDefault walks the registry and asserts the declared and the live rules agree in both directions, so a rules change on the site fails loudly instead of quietly widening what we fetch.

--no-robots

Everything v0.2.0 could do still works with the flag.
It is settable by the flag only, never by config and never by an environment variable, and TestNoRobotsNotInConfig, TestNoRobotsNotInEnv and TestNoRobotsNotImplied hold that.
It warns once on stderr, and the pace floor and any published Crawl-delay still apply either way, since ignoring a stated rate is the part that actually causes harm.

One thing the spec had wrong

Capturing the live file settled a question ~/notes/Spec/3008 had guessed at.
/book/auto_complete?format=json&q= is not disallowed by any rule, is public JSON, carries no key, and returns bookId, workId, title, numPages, avgRating, ratingsCount and the author per hit.
So goodread search keeps working with no flag, and it returns more than it used to.
workId on every hit is the field that makes editions and quotes reachable without a second read.
Registered as s14, guarded by TestSuggestIsAllowed, and the spec is corrected.

Removed

goodread/cookies.go and the --cookies flag.
No login, no Cookie header, no browser driver, no challenge solving.
TestNoCookieHeader, TestNoBrowserDependency and TestNoChallengeHandling keep it that way, and the AWS WAF challenge header is reported as blocked rather than solved.

New command

goodread robots               print the rules and what they permit
goodread robots check <path>  exit 0 allowed, exit 7 disallowed

Verified live against goodreads.com

  • goodread robots -f table prints 29 rules with the /work/editions exception attributed correctly
  • robots check /work/editions/2792775 exits 0
  • robots check '/book/show/2767052?reviewFilters=%7B%7D' exits 7
  • robots check '/search?q=x' exits 7, and exits 0 with --no-robots
  • goodread search "hunger games" returns three real results with no flag
  • goodread shelf 1 is refused with the rule named, and works after one stderr warning with --no-robots

Gate

gofmt -l . clean, go vet ./... clean, go test ./... ok for cli and goodread.
32 new tests, all against the captured robots.txt and httptest servers, none against the live site.

v0.2.0 fetched /search, /review/list and /review/list_rss, all three
disallowed by goodreads robots.txt. It never checked. This adds the
check and puts it somewhere it cannot be walked around.

The check lives in Client.Fetch, not in Client.Do. Do would only cover
call sites ported to the new op registry, and the defect was that call
sites built their own URLs. A check you can skip by not calling the
right function is not a check.

robots.go parses the User-agent: * group with longest-match-wins and
Allow beating Disallow at equal length, which is what the two /work
exceptions depend on: Allow: /work/editions is longer than Disallow:
/work, so editions and quotes stay readable and /work does not.
Matching runs on path plus query, since Disallow: /*reviewFilters can
only ever match a query string. Rules are fetched on first use and
cached for an hour. A fetch failure is exit 8 and nothing is attempted,
because a crawler that carries on when it cannot read the rules is the
thing this change exists to stop.

surfaces.go registers every path the tool knows how to read, so the
policy test can walk the list rather than trusting that somebody
remembered. TestNoDisallowedByDefault asserts the declared and the live
rules agree in both directions, so a rules change on the site fails
loudly instead of silently widening what we fetch.

--no-robots restores everything v0.2.0 could do. It is settable by the
flag only, never by config and never by an environment variable, and
three tests hold that. It warns once on stderr, and the pace floor and
any published Crawl-delay still apply, because ignoring a stated rate
is the part that actually causes harm.

Measuring the live file found something the spec had guessed wrong:
/book/auto_complete is not disallowed, is public JSON, needs no key,
and returns bookId, workId, title, numPages, avgRating and ratingsCount
per hit. So search keeps working with no flag, and it returns more than
it used to.

Cookies are gone. No login, no Cookie header, no browser driver, no
challenge solving, and TestNoCookieHeader, TestNoBrowserDependency and
TestNoChallengeHandling keep it that way. The AWS WAF challenge header
is reported as blocked, not solved.

Part of #8. Closes #9.
@tamnd tamnd added this to the v0.3.0 milestone Aug 15, 2026
@tamnd tamnd added v0.3.0 The v0.3.0 rewrite robots robots.txt fetching, parsing and enforcement breaking Changes behaviour for existing users policy Tests that hold the scope boundary labels Aug 15, 2026
…written

TestNoRobotsNotImplied walks every flag on the command tree, sets each
one, and asserts none of them turns the override on. The failure it
guards against is not somebody wiring --no-robots to --force on
purpose. It is a flag added later that sets a whole config struct at
once and quietly makes the override reachable without anybody typing
it.

TestNoConcurrency pins the transport to one connection per host and
checks that Workers=64 does not widen it. The pace floor is about the
interval between requests, this is about how many are in flight, and a
1s floor with eight connections is eight requests a second as far as
the site is concerned.

Two smaller ones while I was there: TestNoRobotsIsNotHidden, because an
override nobody can see in --help is one people find in a blog post
without the paragraph explaining what it means, and TestCookiesFlagIsGone.
@tamnd
tamnd merged commit 6e1556c into main Aug 15, 2026
4 of 7 checks passed
@tamnd
tamnd deleted the feat/robots branch August 15, 2026 10:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking Changes behaviour for existing users policy Tests that hold the scope boundary robots robots.txt fetching, parsing and enforcement v0.3.0 The v0.3.0 rewrite

Projects

None yet

Development

Successfully merging this pull request may close these issues.

M1: fetch, parse and enforce robots.txt, with --no-robots as the way out

1 participant