Back to all posts
Proxy Header Spoofing in Python: Mimicking Browsers to Bypass Anti-Bot Systems

Proxy Header Spoofing in Python: Mimicking Browsers to Bypass Anti-Bot Systems

September 5, 2026

Why Headers Matter More Than You Think

When you send a request through a proxy, simply changing your IP address isn’t enough. Modern anti-bot systems like Cloudflare, Akamai, and Distil Networks analyze dozens of HTTP headers to determine whether a request comes from a real browser or an automated script. If your headers look suspicious, you’ll get blocked regardless of how clean your proxy is.

Common Anti-Bot Triggers

  • Missing or generic User-Agent: Using python-requests/2.31.0 or no User-Agent at all is a red flag.
  • Incomplete Accept headers: Real browsers send Accept, Accept-Encoding, Accept-Language, and Sec-Fetch-* headers.
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.5
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: none
Sec-Fetch-User: ?1
Upgrade-Insecure-Requests: 1
  • Missing Referer: A direct hit with no Referer looks automated.
  • Unusual header ordering: Real browsers follow predictable header order.

Setting Up Realistic Headers in Python

Basic Header Spoofing with requests

import requests

headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
    "Accept-Language": "en-US,en;q=0.5",
    "Accept-Encoding": "gzip, deflate, br",
    "Sec-Fetch-Dest": "document",
    "Sec-Fetch-Mode": "navigate",
    "Sec-Fetch-Site": "none",
    "Sec-Fetch-User": "?1",
    "Upgrade-Insecure-Requests": "1",
    "Referer": "https://www.google.com/",
}

response = requests.get(
    "https://httpbin.org/headers",
    headers=headers,
    proxies={
        "http": "http://user:pass@proxy-ip:8080",
        "https": "http://user:pass@proxy-ip:8080",
    },
)

print(response.json())

Rotating User-Agents Dynamically

A static User-Agent is easy to detect. Rotate through a pool of real browser signatures:

import random
import requests

USER_AGENTS = [
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36",
    "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36",
]

def get_random_headers():
    return {
        "User-Agent": random.choice(USER_AGENTS),
        "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
        "Accept-Language": "en-US,en;q=0.5",
        "Accept-Encoding": "gzip, deflate, br",
        "Sec-Fetch-Dest": "document",
        "Sec-Fetch-Mode": "navigate",
        "Sec-Fetch-Site": "none",
        "Upgrade-Insecure-Requests": "1",
    }

headers = get_random_headers()
response = requests.get("https://example.com", headers=headers, proxies={...})

Integrating Headers with Rotating Proxies

Session-Level Header Management

Use requests.Session to persist headers across requests while rotating proxies:

from itertools import cycle
import requests

proxy_pool = cycle([
    {"http": "http://user:pass@proxy1:8080", "https": "http://user:pass@proxy1:8080"},
    {"http": "http://user:pass@proxy2:8080", "https": "http://user:pass@proxy2:8080"},
])

session = requests.Session()

def make_request(url):
    proxy = next(proxy_pool)
    headers = get_random_headers()
    
    try:
        response = session.get(url, headers=headers, proxies=proxy, timeout=10)
        response.raise_for_status()
        return response
    except requests.RequestException as e:
        print(f"Request failed with proxy {proxy}: {e}")
        return None

Matching Headers to Proxy Type

Not all headers should be randomized. Some headers reveal inconsistencies:

  • DNT (Do Not Track): Real browsers send DNT: 1 inconsistently. Donu2019t include it unless matching a real browser.
  • Sec-Ch-Ua: This Client Hints header must match the User-Agent version exactly. If you spoof Chrome 120, send Sec-Ch-Ua: "Not A(Brand";v="99", "Chromium";v="120", "Google Chrome";v="120".
headers["Sec-Ch-Ua"] = '\"Not A(Brand\";v=\"99\", \"Chromium\";v=\"120\", \"Google Chrome\";v=\"120\"'
headers["Sec-Ch-Ua-Mobile"] = "?0"
headers["Sec-Ch-Ua-Platform"] = '\"Windows\"'

Advanced Techniques: Browser-Like Behavior

Adding Connection Headers

Real browsers include Connection: keep-alive and Cache-Control: max-age=0:

headers["Connection"] = "keep-alive"
headers["Cache-Control"] = "max-age=0"
headers["TE"] = "Trailers"

Handling Gzip and Brotli Compression

Proxies often strip compression. Handle it gracefully:

import brotli
import gzip
import io

# requests handles gzip automatically, but brotli needs manual decoding
def decode_response(response):
    content = response.content
    if response.headers.get("Content-Encoding") == "br":
        content = brotli.decompress(content)
    return content

Testing Your Setup

Verify Headers with httpbin.org

Always test your headers before deploying:

response = requests.get("https://httpbin.org/headers", headers=headers, proxies=proxy)
print(response.json())

Expected output:

{
  "headers": {
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
    "Accept-Encoding": "gzip, deflate, br",
    "Accept-Language": "en-US,en;q=0.5",
    "Host": "httpbin.org",
    "Sec-Ch-Ua": "\"Not A(Brand\";v=\"99\", \"Chromium\";v=\"120\", \"Google Chrome\";v=\"120\"",
    "Sec-Ch-Ua-Mobile": "?0",
    "Sec-Ch-Ua-Platform": "\"Windows\"",
    "Sec-Fetch-Dest": "document",
    "Sec-Fetch-Mode": "navigate",
    "Sec-Fetch-Site": "none",
    "Upgrade-Insecure-Requests": "1",
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
  }
}

Detecting Detection

If you get a 403, CAPTCHA, or redirect to a block page, inspect the response:

if response.status_code == 403:
    print("Blocked! Headers or proxy detected.")
    print(response.text[:500])  # Check for block page content

Best Practices Summary

  1. Never use default library headers: Always set a full browser fingerprint.
  2. Match header versions: If User-Agent says Chrome 120, ensure Sec-Ch-Una matches.
  3. Rotate headers per request: Donu2019t reuse the same User-Agent across all requests.
  4. Test every proxy-header combination: Some proxies strip headers unexpectedly.
  5. Respect robots.txt and rate limits: Spoofing headers doesnu2019t give you license to abuse sites.

Conclusion

Header spoofing is not about deception for its own sake—it’s about making your automation indistinguishable from legitimate traffic so you can access data ethically and reliably. Combine realistic headers with quality rotating proxies, and always monitor your success rates. When done right, you can collect data at scale without tripping anti-bot systems.

Remember: the goal is not to break security, but to behave like a well-behaved browser that respects the site’s resources.