Bloccare bot e scraper su PrestaShop: i filtri a faccette
6.099 URL filtrate in 24 ore, generate da scraper sulla navigazione a faccette. Come distinguere i bot dai clienti nei log e fermarli senza rompere i filtri.
Su un e-commerce PrestaShop B2B di utensileria industriale il carico anomalo era stato ricondotto alla cache: sistemata quella, il catalogo ha smesso di andare in 503. Solo che gli errori non erano spariti. Si erano spostati, tutti insieme, su un’unica famiglia di URL: le pagine con i filtri e l’ordinamento della navigazione a faccette. In 24 ore quel catalogo aveva servito 6.240 richieste con un filtro attivo, su 6.099 indirizzi diversi. Praticamente ogni richiesta un URL mai visto prima.
Non era un picco di clienti interessati agli abrasivi. Era un catalogo sotto assedio: cluster di macchine su cloud pubblici che lo esploravano combinazione dopo combinazione, con nessuna intenzione di comprare niente.
⚠️ Il sintomo: gli errori si spostano, non spariscono
Dopo il fix della cache, il catalogo “liscio” reggeva: categorie e prodotti serviti da cache anche a distanza di giorni. Ma su ?q=... e ?order=... il quadro era un altro. Quelle URL non sono cachabili per definizione, quindi ogni singola richiesta finiva su PHP, con la sua connessione al database e la sua sessione. Nelle ore di punta si arrivava a 913 errori 503 in una sola ora.
Nella stessa fotografia di 24 ore c’erano altri due numeri che raccontano la stessa storia:
6.240 richieste con filtro attivo -> 6.099 URL distinte (98% mai ripetute)
26.000 richieste ?add=1&id_product=... (bot che seguono i link "aggiungi al carrello")
15.552 risposte 403 (scanner già bloccati, correttamente)
Qualche giorno più tardi lo stesso meccanismo si è presentato con un’altra faccia: 500 intermittenti su tutto il sito, con il pannello del database a circa 120 connessioni aperte. Non era un bug del codice, e si vedeva subito dal fatto che lo stesso URL rispondeva a volte 200 e a volte 500: una risorsa satura, non un errore fatale. Il crawl esaustivo della long-tail aveva riempito il pool MySQL dell’hosting condiviso, e PrestaShop restituiva SQLSTATE[08004] [1040] Too many connections.
Lo stesso URL a volte 200 e a volte 500 non è mai un bug del codice: è una risorsa che finisce. Il codice sbagliato sbaglia sempre.
🛠️ Come si distingue un bot da un cliente negli access log
Questa è la parte che decide tutto il resto: se non separi i due traffici, ogni difesa che metti in piedi colpisce anche chi compra. Sul catalogo in questione i criteri utili sono stati cinque, in ordine di affidabilità.
1. Come arriva la richiesta, non da dove. È il discriminante più forte e il meno usato. Sul tema di quel PrestaShop i filtri per gli utenti veri girano in AJAX: la richiesta porta l’header X-Requested-With: XMLHttpRequest e un parametro dedicato del core (from-xhr). Nessun cliente naviga il catalogo digitando ?q=Scegli-...&order=product.price.asc come pagina intera. Chi chiede il faceted in full page è un bot, un browser senza JavaScript o un link condiviso: tre casi rarissimi e tutti innocui da reindirizzare.
2. La forma del cluster. Un gruppo di 42 indirizzi su una region AWS aveva prodotto 512 richieste, quasi tutte 200, solo su pagine prodotto e categoria, referer sempre vuoto, user agent Chrome che ruotavano. Un altro cluster, su Aliyun, si comportava all’opposto: 151 indirizzi distinti per 175 richieste, cioè un colpo per IP, che è il modo classico di restare sotto qualsiasi soglia di rate-limit.
3. Il PTR, prima di dare per scontato il peggio. Il range che generava lo scraping più aggressivo sui filtri sembrava un blocco residenziale dietro CGNAT, cioè qualcosa da non bloccare mai. Risolvendo il reverse DNS venivano fuori nomi tipo ecs-*.compute.hwclouds-dns.com: macchine virtuali di un cloud pubblico, non famiglie con la fibra. Un host su tre indirizzi ha evitato di scambiare un datacenter per dei clienti.
4. I refusi nello user agent. Le botnet di fascia bassa si tradiscono da sole: Mozlila invece di Mozilla, Bulid, Moblie Safari. Nessun browser vero ha mai scritto così il proprio nome, e una regola su quelle stringhe non produce falsi positivi.
5. Il comportamento dei bot buoni, come metro di paragone. Sugli stessi identici URL filtrati Googlebot prendeva 200, perché rallenta da solo quando il server fatica. Gli errori li raccoglievano solo i client che non rallentano mai. Se il tuo 503 lo prende Googlebot e non lo prende lo scraper, hai puntato l’arma dalla parte sbagliata.
Resta un criterio che viene prima di tutti gli altri: riconoscere i tuoi. In quella giornata l’indirizzo con 1.834 richieste, comprese le più anomale, ero io che stavo testando. Poi c’erano l’ufficio del cliente, il monitor di uptime, i cron e un’integrazione di logistica. Prima di scrivere una sola regola, quegli indirizzi vanno messi in whitelist, altrimenti la prima cosa che blocchi è il lavoro di qualcuno.
⚠️ Perché la navigazione a faccette è un buco di crawl budget
Il numero da cui siamo partiti, 6.240 richieste su 6.099 URL distinte, è la definizione stessa del problema: il 98% di quegli indirizzi non si ripete mai. Ogni combinazione di marca, misura e ordinamento produce una stringa nuova, e una stringa nuova è per forza un cache miss. Miss significa PHP, query, sessione, memoria. Moltiplicalo per qualche migliaio all’ora e il conto arriva al database prima che al web server.
Il danno però non è solo di carico, ed è qui che la faccenda diventa SEO tecnica e non solo sistemistica. Il crawl budget che Google dedica al sito è finito: ogni volta che lo spende su abrasivi?order=product.price.desc&q=Scegli-la-grana-120 non lo sta spendendo sui prodotti nuovi appena caricati a catalogo. Le varianti filtrate non porteranno mai traffico, perché sono quasi-duplicati della categoria base, ma consumano scansione, indice e pazienza.
Vale la pena aggiungere che non sono solo gli scraper malevoli a pesare. Nelle stesse 24 ore AhrefsBot aveva fatto 46.135 richieste e GPTBot 16.554: crawler perfettamente leciti, che rispettano le regole, e che comunque da soli valgono più di tutto il traffico umano di una giornata. Il punto non è “bloccare i bot”, è dire a ciascuno dove può andare. Lo stesso ragionamento lo porto dentro un audit tecnico di un e-commerce: prima si guarda cosa il sito sta effettivamente servendo, poi si decide cosa tagliare.
✅ La soluzione: 301 dei full-page, XHR escluse
La strada che ha risolto il carico e la SEO insieme è una sola regola nel .htaccess, prima del routing di PrestaShop: le richieste full-page con q o order vengono reindirizzate alla URL pulita, le chiamate AJAX no.
RewriteCond %{HTTP:X-Requested-With} !XMLHttpRequest [NC]
RewriteCond %{QUERY_STRING} (^|&)(q|order)= [NC]
RewriteCond %{QUERY_STRING} !(^|&)(ajax|from-xhr) [NC]
RewriteRule ^(.+)$ /$1? [R=301,L]
Il risultato è che il bot riceve la categoria base servita da cache (nessun 503), PHP non genera mai una pagina filtrata, Google consolida le faccette sulla pagina base e i clienti continuano a filtrare esattamente come prima, perché le loro richieste passano dall’AJAX e la regola le lascia stare.
L’alternativa che sembra più elegante, cioè togliere q e order dalla chiave di cache, l’ho scartata perché avvelena la cache, e ho spiegato il meccanismo nel caso della cache LiteSpeed. In sintesi: cambiare la chiave non cambia quello che PHP calcola, quindi una pagina filtrata può finire salvata sotto l’indirizzo della categoria base e venire poi servita a tutti.
L’errore che ho fatto, e come si evita
Il primo deploy di quella regola ha rotto i filtri per gli utenti veri per una sera. Avevo verificato il parametro delle chiamate AJAX con un test da riga di comando e ne avevo dedotto che si chiamasse ajax; nel browser reale, invece, il tema usa from-xhr più l’header X-Requested-With. La condizione di esclusione non intercettava niente, e ogni click su un filtro finiva reindirizzato alla categoria base.
Due lezioni, entrambe costate qualcosa:
- Le XHR si testano nel browser vero, con la scheda Network aperta, non ricostruendo a mano una richiesta con
curl. Quello che un tema manda davvero non è quello che ti aspetti che mandi. - La prima versione va in 302, non in 301. Un 301 lo cachano i browser: se sbagli, ogni utente che ha già toccato quella pagina se lo porta dietro per giorni anche dopo il fix. Con un 302 la svista si auto-risolve al deploy successivo. Il passaggio a 301 è arrivato solo due settimane più tardi, a filtri verificati uno per uno nel browser.
🛠️ Il resto del recinto: robots, 403 mirati, sfida JavaScript
Il redirect toglie il carico ma non riduce il numero di richieste: gli scraper continuano a bussare, e prendono un 301 al posto di un 503. Attorno gli sono stati messi altri tre strati, con obiettivi diversi.
robots.txt, per i bot educati. Sono state aggiunte le direttive Disallow su ?q=, ?order= e ?add=. Qui c’è una trappola che colpisce ogni PrestaShop multilingua: il Disallow: /cart che il CMS genera da solo copre solo il percorso inglese senza prefisso di lingua, quindi i 26.000 add-to-cart su /it/cart?add=... e /de/cart?add=... passavano indisturbati e nessuno se ne accorgeva. Da mettere in conto anche la latenza: i crawler grossi rileggono il file circa una volta al giorno, quindi il calo si vede dopo 24-48 ore, non subito.
403 selettivi per user agent, per gli scraper commerciali. Sono stati chiusi i bot che estraggono cataloghi per rivenderli o per addestrare modelli, tenendo espressamente fuori dal blocco Google, Bing, Applebot e i crawler delle ricerche AI (ChatGPT, Claude, Perplexity): quelli portano visibilità e citazioni, non li voglio bloccare. I due bot SEO commerciali sono finiti in una riga isolata e commentabile, per riattivarli in un secondo se servisse un’analisi.
Deny per range, ma solo dove il rischio è zero. I due blocchi di indirizzi risolti come cloud ECS sono stati chiusi del tutto: nessun cliente reale, nessuna integrazione nota. Il range AWS invece non l’ho toccato, perché lì gira il gestionale di magazzino del cliente: bloccarlo avrebbe fermato gli ordini. Vale come regola generale, non come eccezione: mai un blocco per ASN intero. Su Azure girano i bot di OpenAI e di Bing, su AWS metà dei servizi che i tuoi fornitori usano per parlare con il tuo sito.
La sfida JavaScript, con i suoi limiti dichiarati. Sul faceted era stata messa anche una barriera applicativa: richiesta senza cookie di prova, risposta 503 con Retry-After e uno script che imposta il cookie e ricarica una volta sola. I browser veri la attraversano senza accorgersene, e i motori di ricerca sono esclusi a monte. Funziona, ma uno dei cluster eseguiva il JavaScript: negli stessi log si contano sette coppie 503 seguito da 200 sullo stesso indirizzo nel giro di uno o due secondi. Aveva superato la barriera come un browser qualsiasi.
Sempre da quella barriera sono usciti due difetti che vale la pena conoscere prima di scriverne una. La whitelist basata sulla parola bot dentro lo user agent è banale da falsificare, e infatti è stata ristretta ai token dei motori veri, uno per uno. E il riconoscimento dei parametri fatto con una ricerca di sottostringa produce falsi positivi: un indirizzo che conteneva per caso la sequenza cercata dentro un altro parametro riceveva 503 pur essendo un utente legittimo, finché la condizione non è stata riscritta leggendo il parametro vero e non il testo grezzo della query string.
Il risultato, misurato
- Errori 503 azzerati dall’ora esatta del deploy del redirect: prima centinaia all’ora con un picco di 913, dopo zero, sostituiti da circa 900 redirect all’ora.
- Errori 500 da 109 in un’ora a 0 su 400 richieste controllate, dopo il giro di 403 e la messa in cache delle superfici che ne erano rimaste fuori.
- Catalogo base stabile in cache a distanza di oltre 24 ore: categorie e prodotti serviti senza toccare PHP.
- Filtri utente intatti, verificati nel browser su filtro, ordinamento e paginazione, in tutte le lingue del negozio.
- Lato SEO, tripla copertura sulle faccette: il 301 toglie carico e le de-indicizza, il
noindexgià presente nel tema resta come rete di sicurezza, il canonical punta comunque alla base. Le URL filtrate già in indice escono da sole al ricrawl successivo.
Il runbook che rende la cosa ripetibile
Se dovessi rifarlo su un altro catalogo, l’ordine sarebbe questo. Vale come checklist, e i primi due punti sono quelli che non si possono saltare.
[ ] whitelist prima di tutto: tuoi IP, ufficio cliente, cron, uptime, integrazioni
[ ] separa il traffico: come arrivano i filtri agli utenti veri (XHR? quale parametro?)
[ ] verificalo NEL BROWSER, non con curl
[ ] redirect dei soli full-page, in 302 finché non hai testato tutti i filtri
[ ] robots.txt: ?q= ?order= ?add=, occhio ai path con prefisso di lingua
[ ] 403 per user agent solo sugli scraper, mai su motori e crawler AI
[ ] deny per range solo dove sai che non gira nessuna integrazione del cliente
[ ] dopo 48h rileggi i log: i numeri sono scesi o si sono solo spostati?
L’ultimo punto è il più importante e il più saltato. In questo caso la prima soluzione aveva risolto solo metà del problema, e la metà rimasta si era travestita da problema diverso. Un catalogo sotto assedio non si sistema una volta sola: si legge, si delimita e si ricontrolla, ed è il lavoro di assistenza e ottimizzazione PrestaShop che mi capita di fare più spesso. Se il tuo negozio ha errori che compaiono e spariscono senza motivo apparente, raccontamelo da uno dei canali qui sotto, meglio se con gli access log degli ultimi giorni.
❓ Domande frequenti
Come capisco se le richieste sono di bot o di clienti veri?
Bloccare i bot fa male alla SEO?
Il redirect 301 sui filtri rompe la navigazione degli utenti?
Basta il robots.txt per fermare gli scraper?
Hai un progetto in testa?
Raccontami cosa ti serve o cosa non funziona. Rispondo entro 24 ore, senza preventivi a scatola chiusa.