Repository navigation
Serve robots.txt from the root, and keep crawlers off the comparisons - #291
Conversation
acoustid/web/static/robots.txt has existed for years and nothing ever served it at /robots.txt, so its Disallow has never applied to anything. Verified from outside: /robots.txt returns 200 with 1248 bytes of Cloudflare's generated content-signal comments and zero directives, while /static/robots.txt returns the real file. Crawlers only read the root, so the rule was never in force. It is in force now because this is live: the site has been crawled at up to 830,855 requests/hour, 87% of the resulting errors are on /fingerprint/<id>/compare/<id>, and the pool exhaustion behind that has produced around 1.4 million 5xx since 4 November. The comparison endpoint is a combinatorial surface rather than a set of pages -- every pair of fingerprints is its own URL, the similarity grid on a recording page links up to 144 of them, and each one runs acoustid_compare2 against the database. Served rather than redirected to. Googlebot follows a redirect for robots.txt; the crawlers causing this are the least likely to, and a 200 with the body costs nothing. The rule needs a wildcard, and the obvious shorter forms are both wrong. Disallow: /compare/ matches nothing, because robots.txt matches from the start of the path and the path is /fingerprint/<id>/compare/<id>. Disallow: /fingerprint/ would match, but takes the fingerprint detail pages with it, and keeping those indexed was the call. So the rule is Disallow: /fingerprint/*/compare/ -- standard since RFC 9309 and honoured by the major crawlers. That has a real limit, measured rather than assumed: Python's urllib.robotparser does not implement "*", and reports /fingerprint/1/compare/2 as allowed under this rule. So anything using a pre-RFC-9309 parser ignores the line. Crawl-delay is in the same category -- Bing honours it, Google does not. The tests check the rule against a URL built from the route itself with url_for, rather than against a hardcoded path. That is the pairing that matters: if the comparison route is renamed, the rule silently stops matching and crawling resumes, and the test fails instead. They also assert the fingerprint, recording, track and index pages stay crawlable, since the tempting fix is the broad prefix that quietly de-indexes them. The matching in the tests is hand-rolled because robotparser cannot be the oracle for a rule it does not support. This stops well-behaved crawlers and nothing else. It is the cheap first move, not the fix -- a client ignoring robots.txt needs a rate limit, which is separate and outstanding.
|
No actionable comments were generated in the recent review. Walkthrough
Merge Risk:
|
Serves
robots.txtat the root and excludes the fingerprint comparison URLs.The file was never being served
acoustid/web/static/robots.txthas existed for years, and nothing ever served it at/robots.txt— only/favicon.icohad a route. Verified from outside, cache-busted:Crawlers only read the root, so that
Disallow: /data/has never applied to anything.Why now
The site is being crawled hard: up to 830,855 requests/hour, 87% of the resulting errors on
/fingerprint/<id>/compare/<id>, and the pool exhaustion behind that has produced roughly 1.4 million 5xx since 4 November. Diagnosis from acoustid-admin, who also confirmed the origin 404 independently against the web pod.The comparison endpoint is a combinatorial surface rather than a set of pages — every pair of fingerprints is its own URL, the similarity grid on a recording page links up to 144 of them, and each one runs
acoustid_compare2against the database.The rule, and why the two shorter forms are wrong
Disallow: /compare/matches nothing. robots.txt matches from the start of the path, and the path is/fingerprint/<id>/compare/<id>. This is the literal reading of "block the /compare/ endpoints" and it would have been a silent no-op.Disallow: /fingerprint/would work and is honoured by strictly more crawlers, but takes the/fingerprint/<id>detail pages with it. Keeping those indexed was the explicit call; the broader rule stays available if the narrow one proves insufficient.So the rule needs the wildcard, which is standard since RFC 9309 and honoured by the major crawlers.
Its limit, measured rather than assumed: Python's
urllib.robotparserdoes not implement*— under this rule it reports/fingerprint/1/compare/2as allowed. So "wildcards are an extension" is not theoretical; a mainstream stdlib parser ignores the line outright. Anything built on a pre-RFC-9309 parser is unaffected by this change.Crawl-delay: 10is in the same category — Bing honours it, Google does not.Served rather than redirected to: Googlebot follows a redirect for robots.txt, but the crawlers causing this are the least likely to, and returning the body with a 200 costs nothing.
Tests
Six tests. The important one checks the rule against a URL built from the route itself via
url_for, not against a hardcoded path — if the comparison route is ever renamed, the rule silently stops matching and crawling resumes, and this fails instead. Others assert the fingerprint, recording, track and index pages stay crawlable, because the tempting fix is the broad prefix that quietly de-indexes them.Each was mutation-tested: using the literal
/compare/rule, blocking/fingerprint/wholesale, dropping the rule, and removing the root route all fail the suite.The path matching in the tests is hand-rolled, because
robotparsercannot be the oracle for a rule it does not implement.Full suite 426 passed, 1 skipped. Lint clean.
Two things this does not do
It stops well-behaved crawlers and nothing else. A client ignoring robots.txt needs a rate limit on that route, which is separate and still outstanding. Treat this as the cheap first move.
Cloudflare may mask it. It is currently generating the public robots.txt. It will most likely append its content signals to a real origin file once one exists, but it might keep overriding. So after deploy, verify from outside rather than against the origin:
If no
Disallowlines appear there, the app change is being shadowed and it needs a Cloudflare-side fix instead.