Main Ecosystems
HomePython CorePandas ReferenceNumPy ScientificFastAPI & PydanticDjango Enterprise
More Ecosystems
Environment & SetupRequests & HTTPAsyncIO ConcurrencyObject-Oriented OOPPyTorch Deep LearningScikit-Learn MLFlask FrameworkWeb ScrapingDatabase & ORMDevOps & Docker

403 Forbidden Error When Web Scraping Job Sites in Python

Verified FixPython 3.10+Requests 2.31+ / Playwright 1.42+Silo: scraping

Quick Fix / Solution Rapide

Add realistic browser User-Agent and Accept headers, maintain sessions with cookies, or switch to headless Playwright/Selenium for Cloudflare-protected sites.

Root Cause Analysis

This error occurs when a web server or Web Application Firewall (WAF) such as Cloudflare, Akamai, or Datadome blocks an automated scraping script from accessing online job portals (e.g. LinkedIn, Indeed, Glassdoor), returning HTTP status code 403 Forbidden.

1. Default Python Requests User-Agent Header

Python's requests library sends User-Agent: python-requests/2.31.0 by default. Anti-bot firewalls immediately flag and block this signature on sight.

2. TLS Fingerprinting (JA3 / JA4 Fingerprints)

Advanced protection systems inspect the TLS Client Hello packet (cipher suites, elliptic curves, extensions). Python's OpenSSL TLS stack has a recognizable fingerprint distinct from real Google Chrome or Mozilla Firefox browsers.

3. JavaScript Challenge and Cloudflare Turnstile

Modern job boards render content dynamically and serve JavaScript challenges that raw HTTP clients (requests, urllib) cannot evaluate.

4. Rate Limiting and IP Reputation

Sending hundreds of concurrent requests without delays triggers automated IP blocking and temporary subnet bans.

Reproduction Code (MCVE)

Example: Bug Reproduction
# **Note de reproductibilité :** Dépendant des règles de sécurité de la cible.
import requests

resp = requests.Response()
resp.status_code = 403
resp.url = 'https://job-board.example.com/api/jobs'
resp.raise_for_status()

Solution 1: Inject Realistic Browser Headers and Session State

Configure a requests.Session with realistic desktop browser headers to pass basic WAF checks.

Example: Recommended Solution
import requests

session = requests.Session()
session.headers.update({
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.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-US,en;q=0.9',
    'Sec-Fetch-Dest': 'document',
    'Sec-Fetch-Mode': 'navigate'
})
print('Session headers configured with authentic browser signature.')

Solution 2: Use Headless Browser (Playwright / Camoufox)

Use Playwright or undetected automation drivers to execute JavaScript challenges and render dynamic content.

Example: Alternative Solution
print('Playwright scraping pattern:')
print('from playwright.sync_api import sync_playwright')
print('with sync_playwright() as p:')
print('    browser = p.chromium.launch(headless=True)')
print('    page = browser.new_page()')
print('    page.goto("https://example.com")')
print('    print(page.title())')

A common mistake is scraping without adding random request delays (time.sleep(random.uniform(1.5, 4.0))). Sending requests at mechanical intervals (e.g. exactly every 1.0 second) triggers bot detection algorithms even with valid headers. Always obey robots.txt and review the platform's Terms of Service. Edge cases occur with rotating proxies: ensure proxy pools use residential IPs rather than known datacenter subnets (AWS/DigitalOcean). Contrast this error with requests.exceptions.HTTPError: 401 Unauthorized, which indicates missing API authentication tokens.