403 Forbidden bij Web Scraping: De Oplossingsladder die Echt Werkt
Een 403 is geen toestemmingsprobleem — het is een detectieprobleem. Vier sporten scheiden een geblokkeerd script van een 200, en de meeste mensen stoppen bij de eerste.
Een 403 forbidden fout bij web scraping betekent bijna nooit wat de statuscode zegt. HTTP 403 wordt gedefinieerd als de server die je verzoek begrijpt en weigert het te autoriseren — maar wanneer het een scraper raakt, heeft het zelden iets te maken met permissies of een ontbrekende login. Het betekent dat de site naar je verzoek keek, besloot dat een machine het stuurde, en de deur sloot. Dat vertelt je wat je moet veranderen: niet je inloggegevens, maar de vorm van je verkeer. Hieronder is de oplosladder, van goedkoopste sport eerst, met de controles die identificeren op welke sport je vastzit.
403 vs 401 vs 429: wat elk je vertelt
Krijg de diagnose goed voordat je code schrijft. Een 401 Unauthorized vraagt om inloggegevens — deze verstrekken lost het op. Een 403 Forbidden weigert ongeacht inloggegevens, dus inloggen verandert niets als botdetectie het heeft geactiveerd. Een 429 Too Many Requests gaat over volume en herstelt zichzelf wanneer het venster opnieuw instelt; een 403 gaat over identiteit en blijft bestaan totdat je verandert hoe je verzoek eruitziet. Als je scraper 429's krijgt in plaats van 403's, is de oplossing tempo, niet vermomming — we behandelen dat in het oplossen van 429 too many requests.
Diagnoseer in 60 seconden, voordat je code verandert
Drie commando's vertellen je bijna alles. Voer het kale verzoek uit, voer het opnieuw uit met alleen een browser User-Agent verwisseld, en lees dan de response body — de blokkeringsreden staat er meestal in geschreven.
# 1. Bare request: what does the target give a naked client?
curl -sS -o /dev/null -w '%{http_code}\n' https://target.example/page
# 2. Same request, browser User-Agent only
curl -sS -o /dev/null -w '%{http_code}\n' \
-A 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36' \
https://target.example/page
# 3. Read the body and the response headers - the reason is in there
curl -sS -D - https://target.example/page | head -c 600
Interpreteer het als volgt. Als stap 2 200 retourneert, is het hele probleem headers en ben je klaar bij sport 1. Als de body Cloudflare, een Ray ID of Error 1020 vermeldt, zit je achter een WAF-regel — zie Cloudflare error 1020. Een gewone Apache of nginx Forbidden-pagina betekent meestal een servermodule zoals mod_security, die bekende bot User-Agents heeft geblokkeerd sinds lang voordat moderne botbeheer bestond. En als een browser op dezelfde machine de pagina laadt terwijl je client dat niet doet, werk dan door curl 403 maar de browser werkt.

Sport 1: stop met jezelf aan te kondigen in de headers
Python's urllib identificeert zichzelf als iets als python-urllib/3.3.0; requests stuurt python-requests/2.x. Die strings zijn een bekentenis, en het meest gestemde antwoord op de canonieke Stack Overflow-thread over 403's in Python scraping is simpelweg: stuur een browser User-Agent in plaats daarvan. Dat werkt nog steeds op veel sites. Maar twee dingen zijn veranderd sinds dat antwoord werd geschreven. Ten eerste, een kale Mozilla/5.0 is nu zelf een vlag — commentatoren op diezelfde thread melden dat sites het volledig blokkeren, omdat geen echte browser een UA met twee tokens stuurt. Ten tweede, moderne servers vergelijken je hele header set, niet één veld.
Stuur een coherent set: een actuele browser UA, de bijpassende Accept keten, een taal, en de Sec-Fetch-* metadata headers die Chromium aan elke navigatie toevoegt. Header volgorde is ook belangrijk bij strengere doelen — gebruik een geordende mapping en plaats ze in de volgorde die een browser gebruikt.
import requests
HEADERS = {
"User-Agent": (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
),
"Accept": (
"text/html,application/xhtml+xml,application/xml;q=0.9,"
"image/avif,image/webp,*/*;q=0.8"
),
"Accept-Language": "en-GB,en;q=0.9",
"Accept-Encoding": "gzip, deflate", # add 'br' only if brotli is installed
"Upgrade-Insecure-Requests": "1",
"Sec-Fetch-Dest": "document",
"Sec-Fetch-Mode": "navigate",
"Sec-Fetch-Site": "none",
"Sec-Fetch-User": "?1",
"Connection": "keep-alive",
}
with requests.Session() as s:
s.headers.update(HEADERS)
r = s.get("https://target.example/page", timeout=20)
print(r.status_code, len(r.content))
Een coherentieval vangt bijna iedereen: een UA die beweert een US Chrome desktop te zijn, gecombineerd met Accept-Language: de-DE en een exit IP in Brazilië, is een mismatch die elk fatsoenlijk systeem opmerkt. Zorg dat user agent, taal en IP-geografie hetzelfde verhaal vertellen.
Sommige 403's op deze sport zijn zelfs eenvoudiger: een ontbrekende Referer. Servers die alleen een asset serveren wanneer het verzoek eruitziet alsof het van hun eigen pagina kwam, zullen 403 retourneren bij een directe hit en 200 zodra je de verwijzende URL toevoegt. Het is een klassieker, en het kost één header om te testen.
Sport 2: de TLS-vingerafdruk headers kunnen niet oplossen
Als perfecte headers nog steeds 403 retourneren, gebeurde de blokkering voordat je headers zelfs maar werden gelezen. Elke HTTPS-client kondigt zijn cipher suites, extensies, elliptische krommen en ALPN aan in de TLS ClientHello, en die combinatie wordt gehasht in een JA3 of JA4 vingerafdruk. Python's requests, Go's net/http en standaard curl hebben elk een kenmerkende die geen enkele anti-bot leverancier moeite heeft te onderscheiden van Chrome. Zich voordoen als Chrome 131 in een header terwijl je handshakes als OpenSSL is de luidste tegenspraak die een scraper kan maken.
De oplossing is een client die zich voordoet als een echte browser op de TLS-laag. In Python is dat curl_cffi, een binding naar een gepatchte libcurl die browser ClientHellos reproduceert:
# pip install curl_cffi
from curl_cffi import requests as cffi
proxy = "http://USER:PASS@gate.quantumproxies.io:8000"
r = cffi.get(
"https://target.example/page",
impersonate="chrome", # Chrome JA3/JA4 + HTTP/2 settings
proxies={"http": proxy, "https": proxy},
timeout=20,
)
print(r.status_code)
Node heeft equivalenten gebouwd op dezelfde gepatchte TLS-stacks. Als je de volledige mechanica wilt begrijpen waarom deze ene verandering een 403 naar een 200 op beschermde sites omdraait, lees dan hoe JA3/JA4 fingerprinting je scraper verraadt.
Sport 3: het IP is de boodschap
Headers en TLS beschrijven de client. Het IP beschrijft wie er vraagt, en het weegt zwaar mee. Adressen in hosting- en cloudbereiken zijn gepubliceerd, gemakkelijk in kaart te brengen door ASN, en hebben een lagere vertrouwensscore voordat een enkel byte van je verzoek wordt geïnspecteerd — wat de reden is waarom een scraper op een VPS 403's krijgt die dezelfde code op een thuisverbinding moeiteloos doorstaat. Residentiële adressen behoren tot consumenten-internetproviders en worden behandeld als mensen; mobiele carrier IP's zitten achter CGNAT met duizenden echte abonnees elk, wat ze de moeilijkste maakt om in bulk te blokkeren.
Dus sport 3 is een wissel, geen herschrijving: stuur hetzelfde goed gevormde verzoek vanuit een residentiële exit. QuantumProxies beheert 90M+ residentiële IP's in meer dan 200 landen met rotatie per verzoek of sticky sessies, HTTP en SOCKS5 op elk plan, en pay-per-GB facturering — één configuratieregel verandert het ASN dat je doel ziet. Al op residentieel en nog steeds geblokkeerd? Controleer de reputatie van de pool met onze gratis IP kwaliteitsscore checker: alles boven de 75 is verbrand en zal 403's verzamelen, ongeacht hoe goed je headers zijn.
Sla de ladder over: haal elke pagina op met de Scraper API

Sport 4: rendering en uitdagingen
De laatste sport is de dure. Sommige 403's zijn de zichtbare helft van een JavaScript-uitdaging: de server levert een klein script, verwacht binnen enkele seconden een antwoord, en weigert alles wat het niet kan uitvoeren. Geen enkele header set en geen IP lost dat op, omdat de test is of je code kunt uitvoeren. Opties in oplopende kosten: een headless browser met een stealth patch set, een beheerde browserpool, of een scraping API die op aanvraag rendert. Rendering kost vele malen meer per pagina dan een gewoon HTTP-verzoek, dus escaleer alleen voor de URL's die het echt nodig hebben.
De QuantumProxies Scraper API voegt sporten 2 tot 4 samen in één verzoek: browser-grade TLS, residentiële exits, JavaScript-rendering wanneer een pagina het nodig heeft, en markdown, JSON of ruwe HTML terug. Dat is de eerlijke ruil — je stopt met het onderhouden van de ladder en betaalt per succesvolle pagina in plaats daarvan.
De ladder in de praktijk gebruiken
- Test opnieuw na elke enkele verandering. Twee veranderingen tegelijk en je leert niets over welke werkte.
- Cache de ruwe 403 body tijdens ontwikkeling. Blokkeerpagina's bevatten Ray ID's, regelnamen en leverancier vingerafdrukken die je tegenstander benoemen.
- Behandel een 403 bij het allereerste verzoek als een identiteitsprobleem; een 403 die verschijnt na 200 goede pagina's is een reputatie- of tempo probleem.
- Probeer een 403 hooguit één keer opnieuw, en alleen na iets te hebben veranderd. Hetzelfde verzoek vanaf hetzelfde IP blijven sturen verandert een zachte blokkering in een harde.
- Als er een openbare API of feed bestaat voor dezelfde gegevens, gebruik die. Het is goedkoper dan elke sport hierboven en het breekt nooit bij een herontwerp.
Banpreventie is de zusterdiscipline: zodra je een 200 hebt, is het behouden ervan een kwestie van tempo, sessiehygiëne en poolgezondheid in plaats van vermomming.
Veelgestelde vragen
Wat veroorzaakt een 403 forbidden fout bij web scraping?
Detectie, in bijna elk geval. De gebruikelijke triggers zijn een standaard bibliotheek User-Agent, een onvolledige of tegenstrijdige header set, een datacenter IP met een slechte reputatie, een verzoektempo dat te regelmatig is, of een TLS-vingerafdruk die niet overeenkomt met de browser die je beweert te zijn. Echte toestemmingsfouten bestaan, maar ze retourneren ook 403 aan browsers — test in een voordat je het aanneemt.
Hoe omzeil ik een 403 forbidden fout in Python?
Werk de ladder af. Voeg een volledige browser header set toe aan een requests.Session; als dat faalt, schakel dan over naar een client die browser TLS nabootst zoals curl_cffi; als dat faalt, routeer dan via een residentiële proxy; als de pagina een JavaScript-uitdaging verzendt, render het of gebruik een scraping API. Escaleer alleen wanneer de goedkopere sport daadwerkelijk is getest.
Waarom retourneert httpx 403 wanneer mijn browser dat niet doet?
Dezelfde reden als requests: httpx stuurt een minimale header set en een Python TLS-vingerafdruk. Kopieer het exacte verzoek van de browser uit DevTools, speel het opnieuw af met httpx, en de 403 verdwijnt meestal — wat je vertelt dat het verschil headers was. Als het aanhoudt met identieke headers, zit de blokkering op de TLS- of IP-laag.
Hoe los ik 403 forbidden op in Scrapy?
Stel een realistische DEFAULT_REQUEST_HEADERS plus USER_AGENT in, houd ROBOTSTXT_OBEY eerlijk over wat je mag ophalen, schakel AUTOTHROTTLE_ENABLED in, en routeer verzoeken via een roterende proxy middleware. Scrapy retries proberen standaard geen 403 opnieuw — voeg het toe aan RETRY_HTTP_CODES alleen als je het IP tussen pogingen roteert.
Een 403 is informatie, geen muur. Het vertelt je welke van de vier signalen je verraadden, en elke sport van de ladder kost meer dan die eronder. Begin bij de goedkoopste, test na elke verandering, en stop met klimmen zodra de statuscode 200 wordt.