chadtmiller.com/knotless
git that lives in your Atmosphere account
serve holds crawlers to robots.txt
MergedThis makes the pages proxy return 403 to a bot, crawler or spider User-Agent that asks for a path web/static/robots.txt disallows. It reads the same file through include_str! and matches it with *, $, decoded escapes, longest rule wins, and Allow winning a tie. Link previewers are exempt.
I traced the matcher against the shipped rules and found no bug. * runs, a final $ after a * part, the closed-list Allow rules beating Disallow: …issues?q=, and tree/main beating tree/ all come out as the new test expects. The twelve test cases cover both outcomes and the main edges. The design page and the robots.txt comment are updated, and no change file is needed because only serve changes.
Two minor notes, neither blocking:
- Because both sides are decoded, a crafted
q=state%3Aclosed%26label:xdecodes toq=state:closed&label:x. That still hits theDisallow, but a%26cursor=smuggled into the value would hit theAllowfor&cursor=. Real crawlers decode the same way, so this only matters for an adversarial one. - The
botsubstring check will also catch any in-house monitor whose User-Agent contains "bot". That only matters if one fetches a disallowed page.
Carried forward: the change this pull makes at 4748f8c320de2b242ac8cf2a11f48fda5dc3b36b is the one reviewed at 0e0704b36ed2db866e1d6c1c2a38eb597b011578, so that review stands. What is new comes from its base.
8 passed, 2 skipped
What changes
serve holds crawlers to robots.txt. A page request whose
User-Agentsays it is a bot, a crawler or a spider, and whose path and queryweb/static/robots.txtdisallows, gets a 403 before it is handed to the pages. serve reads the same file, built in withinclude_str!, so the two cannot drift. It matches the rules as crawlers do:*for any run,$for the end, escapes decoded on both sides (soq=state:closedandq=state%3Aclosedmatch the same rule), and the longest matching rule deciding, withAllowwinning a tie.A link preview (Slack, Discord, Twitter, Telegram, WhatsApp, LinkedIn, embed services) is not held to it: someone shared that link, and a commit or check link pasted in a chat should still unfurl. Browsers, the CLI, and the harness's own clients are never matched.
Why
The robots rules for list filters landed yesterday (Crawlers skip list filters, and a page read retries a lost connection), and left a rate limit out until they were measured. Measured now, from the request logs:
/chadtmiller.com/knotless/issues?q=…with up to nine label and author terms, all disallowed.Turning these away in serve costs a string match, with no page drawn and no
listItemsasked. A 403 rather than a 429: a 429 tells Google to slow its whole crawl, and these URLs should not be crawled at all. Doing it in serve rather than Caddy keeps it tested, and it ships with each deploy, with nothing to change on the host.How it was checked
pages_drawn_elsewherenow sends twelve requests through the pages proxy:__data.json, a non-main tree,/search, and a closed filter with another term.tree/main, and a profile.The build, clippy and coverage are left to CI.
Shortcomings
A crawler that calls itself a browser is not caught. One that says neither bot, crawl nor spider is not either. Only page requests are checked: an XRPC call a crawler makes directly is answered as before. A new link previewer that names itself a bot gets a 403 on disallowed pages until it is added to the list.
🤖 Generated with Claude Code