← Back to blog

Stop 407 Errors and DNS Leaks: Python Requests Proxies for Scrapers

September 14, 2026
Stop 407 Errors and DNS Leaks: Python Requests Proxies for Scrapers

Pass a proxies dictionary to requests.get(), requests.post(), or a Session object, mapping the http and https schemes to your proxy URL. Requests also reads HTTP_PROXY, HTTPS_PROXY, and related environment variables automatically. For SOCKS proxies, install requests[socks] first, since Requests needs PySocks to handle that protocol. The two mistakes that trip up almost everyone: forgetting the scheme prefix on the proxy URL, and assuming Requests trusts an intercepting proxy's certificate the same way a browser does. The examples below cover both, plus authentication, rotation, and the errors you'll actually hit in production.


TL;DR:

  • Always include the scheme prefix like http:// or socks5h:// in proxy URLs to prevent connection errors and ensure requests use the intended proxy.
  • Environment variables such as HTTP_PROXY and HTTPS_PROXY can override explicit proxy settings; set NO_PROXY='*' during testing to avoid unintentional proxy bypasses.
  • Use urllib.parse.quote to properly encode proxy credentials with special characters, preventing misparsing that leads to authentication failures or 407 errors.
  • For SOCKS proxies, prefer socks5h:// to avoid DNS leaks by letting the proxy handle hostname resolution instead of the client machine.
  • Rely on managed proxy providers when scaling or needing geographic targeting, as building and maintaining rotation and health checks is complex and prone to failure.

Natproxies
Scale Scraping With Reliable Proxies
NatProxies provides static ISP and rotating residential proxies with geographic targeting for scraping, automation, and SEO analysis.
Explore NatProxies

Table of Contents

Python Requests Proxies: The Basic Dict and How to Verify It Works

The simplest working proxy setup in Requests looks like this:

import requests

proxies = {
    'http': 'http://198.51.100.10:8080',
    'https': 'http://198.51.100.10:8080',
}

response = requests.get('https://httpbin.org/ip', proxies=proxies)
print(response.json())

That's the entire API surface for basic usage. You build a dictionary keyed by scheme, point each key at a proxy URL, and pass it as a keyword argument to whatever request method you're calling. Requests figures out the rest.

The part that trips up newcomers is the scheme prefix. Write 198.51.100.10:8080 without the http:// in front, and Requests throws MissingSchema: Invalid URL '198.51.100.10:8080': No scheme supplied. The Requests advanced usage documentation is explicit about this: proxy URLs need a scheme just like any other URL Requests handles, because internally it treats the proxy address the same way it parses the target URL. Always write out http:// or socks5://, even when the proxy itself only speaks HTTP.

Confirming the proxy actually did something matters more than people think, because a misconfigured proxy dict often fails silently, your traffic goes out through your normal connection, and you never notice until a target site flags your real IP address. The fix is a one-line sanity check. Hit an IP-echo endpoint before and after adding the proxy:

# Without proxy
direct = requests.get('https://httpbin.org/ip').json()
print('Direct IP:', direct['origin'])

# With proxy
proxied = requests.get('https://httpbin.org/ip', proxies=proxies).json()
print('Proxied IP:', proxied['origin'])

If those two IP addresses match, your proxy isn't being used. Common causes: a typo in the dictionary key (it's https, not HTTPS or Https), a firewall blocking the proxy port, or an environment variable silently overriding what you just set (more on that in the next section).

Authentication follows the same URL pattern you'd use in a browser, embedding credentials directly in the userinfo portion of the proxy URL:

proxies = {
    'http': 'http://username:password@198.51.100.10:8080',
    'https': 'http://username:password@198.51.100.10:8080',
}

A few things worth knowing before you copy this into production code:

  • The username and password go before the @, separated by a colon, exactly like basic auth in any URL.
  • Special characters in credentials (@, :, /, %) must be percent-encoded with urllib.parse.quote(), or Requests will misparse the URL and either fail outright or send the wrong string as your password.
  • You can set both http and https keys to the same proxy URL if your provider handles both protocols on one port, which most modern proxy services do.
  • Testing this against httpbin.org/ip first, before pointing it at your actual target, isolates whether a failure is about the proxy or about the site you're scraping.

Once the basic dict works and you've confirmed the outward IP has changed, you're ready to think about how Requests decides between session settings, environment variables, and per-call arguments when more than one is present.

Sessions vs. Environment Variables: Which One Wins?

Per-request proxies arguments generally take precedence over session.proxies, but environment variables can override both if you're not careful. This is the single most common source of "why is my proxy config being ignored" bug reports in Requests-based scrapers.

A Session object lets you set proxies once and reuse them across many requests, which also gives you connection pooling, meaning Requests keeps TCP connections alive between calls instead of renegotiating a new one every time:

session = requests.Session()
session.proxies.update({
    'http': 'http://198.51.100.10:8080',
    'https': 'http://198.51.100.10:8080',
})

response = session.get('https://httpbin.org/ip')

This is faster than passing a fresh proxies dict to every single call when you're making hundreds of requests to the same host, since the underlying connection gets reused rather than rebuilt.

Here's where it gets messier. Requests doesn't operate in isolation. It calls into Python's urllib.request.getproxies() under the hood to detect system-level proxy settings, and that function checks environment variables and, on some platforms, OS-level configuration. The urllib.request documentation notes that lowercase environment variables (http_proxy, https_proxy) are generally preferred over uppercase ones, and that macOS can pull proxy settings from System Configuration, while Windows can read them from the registry, even if you never set an environment variable yourself.

The practical precedence order works out like this:

  1. A proxies argument passed directly to a request method wins over session-level settings for that single call.
  2. session.proxies applies to every request made through that session object, unless overridden per call.
  3. Environment variables (HTTP_PROXY, HTTPS_PROXY, NO_PROXY) get consulted by Requests' internal rebuild_proxies logic, and per the Requests API documentation, can end up stripping or replacing proxies for URLs that match a NO_PROXY pattern, even on requests originating from a session with proxies already configured.

If you want to eliminate environment interference entirely during a test run, set NO_PROXY='*' (or no_proxy='*' on macOS, where lowercase is preferred) before your script runs, which tells the underlying resolution logic to skip proxying for every host.

The pattern worth adopting for anything you're deploying, not just testing locally: pass proxies explicitly per request or per session, and don't rely on ambient environment variables to do the job. Environment-based configuration is convenient for quick one-off scripts, but it's the wrong choice the moment you need deterministic, auditable routing, say, when different scraping jobs need different proxies and you can't risk one job silently picking up another's environment settings.

Handling Proxy Authentication and Fixing 407 Errors

A 407 Proxy Authentication Required response almost always means one of three things: missing credentials, a malformed userinfo string, or characters in your password that broke the URL parsing.

Correct proxy authentication in Requests goes straight into the URL, following the same format covered earlier:

proxies = {
    'http': 'http://scraper01:xk9!mq2@198.51.100.10:8080',
    'https': 'http://scraper01:xk9!mq2@198.51.100.10:8080',
}

That password has an exclamation point in it, and that's exactly the kind of thing that breaks silently. The ! isn't a reserved URL character, but @, :, /, and % are, and if your credential-generation script ever produces one of those, the URL parser will misread where the username ends and the host begins. Encode credentials with Python's standard library before building the dict:

Illustration of proxy credential parsing

from urllib.parse import quote

user = quote('scraper01', safe='')
pwd = quote('p@ss:word/123', safe='')
proxy_url = f'http://{user}:{pwd}@198.51.100.10:8080'

When you get a 407 instead of a successful response, work through this list before assuming the proxy itself is broken:

  • Confirm the credentials are current. Proxy providers rotate or expire login details more often than most people expect.
  • Check for accidental whitespace or newline characters if you're loading credentials from an environment variable or a config file.
  • Reproduce the failure with curl outside Python: curl -x http://user:pass@host:port https://httpbin.org/ip. If curl also gets a 407, the problem is the proxy or credentials, not your Requests code.
  • Verify you're not mixing up proxy authentication with target-site authentication. A 407 is the proxy rejecting you; a 401 or 403 from the actual site is a different problem entirely.
  • Ask your proxy provider whether authentication is IP-based (they whitelist your server's IP) rather than credential-based. If so, embedding a username and password in the URL does nothing, and you need to have your outbound IP added to their allowlist instead.

Pro Tip: Never hard-code proxy credentials directly in a script that gets committed to version control. Pull them from environment variables or a secrets manager at runtime, and add your config files to .gitignore before you write the first line of proxy code, not after you accidentally push a password to a public repository.

Storage discipline matters here more than it does for most other API keys, because a leaked proxy credential can let someone else route traffic through your paid bandwidth allocation without your knowledge. NatProxies's troubleshooting guide on 407 errors walks through provider-side causes if you've ruled out everything on the client side and the errors persist.

Setting Up SOCKS Proxies With requests[socks]

SOCKS proxy support in Requests isn't built in by default. You need to install the optional PySocks dependency first, using:

python -m pip install 'requests[socks]'

Once that's installed, using a SOCKS proxy looks almost identical to the HTTP dictionary pattern, just with a different scheme:

proxies = {
    'http': 'socks5://198.51.100.10:1080',
    'https': 'socks5://198.51.100.10:1080',
}

response = requests.get('https://httpbin.org/ip', proxies=proxies)

The detail that catches experienced developers off guard is the difference between socks5:// and socks5h://, and it comes down to where DNS resolution happens:

  1. socks5:// resolves the target hostname on the client side, meaning your machine's DNS resolver looks up the IP address before the request ever reaches the proxy. That's a DNS leak: even though your traffic is proxied, whatever service handles your DNS queries can see which domains you're visiting.
  2. socks5h:// sends the hostname itself to the proxy and lets the proxy handle DNS resolution. Nothing about which domains you're hitting leaks through your own DNS resolver. According to Requests' own guidance on SOCKS behavior, using socks5h is the safer default whenever DNS leakage is a concern, which for scraping and automation work is essentially always.
  3. Troubleshooting DNS-related failures with SOCKS proxies usually starts with checking whether you used socks5 when you meant socks5h. If a request works fine to an IP address but fails or times out against a hostname, that's the tell. Also confirm PySocks actually installed correctly, since a partial or failed install of requests[socks] throws a MissingDependencyError that's easy to misread as a network problem.

For most scraping and automation scenarios, default to socks5h:// unless you have a specific reason to resolve DNS locally, such as needing to hit an internal hostname your proxy provider can't resolve.

Why HTTPS Proxies Throw SSL Errors (and How to Fix Them)

An SSLError when routing HTTPS traffic through a proxy almost always means the proxy is intercepting and re-signing your TLS connection with its own certificate, one Requests doesn't recognize as trustworthy.

This happens most often with corporate network proxies, security-scanning proxies, or debugging tools like Charles Proxy and mitmproxy, which sit in the middle of your connection and present a self-generated certificate instead of passing the origin server's real one through. Requests validates every HTTPS connection against its bundled root certificate store by default, and an unfamiliar intercepting certificate fails that check immediately.

The Stack Overflow thread on this exact failure mode documents the standard fix: point Requests at a CA bundle that includes the proxy's certificate, rather than disabling verification entirely.

Requests exposes a few ways to handle this:

  • Find where Requests looks for certificates by default. The library ships with certifi, and you can inspect the active bundle path by running import certifi; print(certifi.where()) in a Python shell.
  • Set the REQUESTS_CA_BUNDLE environment variable to point at a bundle file that includes your proxy's certificate, appended to the standard trusted roots. Requests reads this variable automatically on every call.
  • Pass verify='/path/to/cacert.pem' directly to a request or session if you'd rather not set a global environment variable, which is often the cleaner option in CI pipelines where environment state gets messy.
  • Avoid verify=False as a permanent fix. It suppresses the error by disabling certificate validation entirely, which also disables protection against a genuine man-in-the-middle attack. It's fine for a five-minute local debugging session; it's a liability left in production code, especially anything handling credentials or personal data.

The one caveat worth flagging clearly: whichever fix you choose, the recommended path is installing the proxy's CA certificate into a trusted bundle rather than turning verification off, because the entire point of TLS validation is confirming you're actually talking to the server you think you are.

Proxy Rotation Strategies That Actually Reduce Blocking

Sticky sessions keep you on the same IP address across multiple requests, which login flows and multi-step checkout processes generally need, while rotating proxies switch IPs frequently, which high-volume scraping generally needs to avoid tripping rate limits or bot detection on a single address.

Picking the wrong one for the job causes most of the blocking problems developers blame on "bad proxies." A login flow that rotates IPs mid-session looks like account takeover to most platforms and gets flagged immediately. A high-throughput scraper stuck on one sticky IP for thousands of requests looks like a bot hammering a single address, which is exactly what it is.

Here's a practical rotation pattern for cycling through a proxy list per request:

import itertools
import requests

proxy_list = [
    'http://user:pass@proxy1.example.com:8080',
    'http://user:pass@proxy2.example.com:8080',
    'http://user:pass@proxy3.example.com:8080',
]
proxy_cycle = itertools.cycle(proxy_list)

def get_with_rotation(url, max_attempts=3):
    last_exception = None
    for _ in range(max_attempts):
        proxy_url = next(proxy_cycle)
        proxies = {'http': proxy_url, 'https': proxy_url}
        try:
            response = requests.get(url, proxies=proxies, timeout=10)
            if response.status_code == 200:
                return response
        except requests.exceptions.ProxyError as e:
            last_exception = e
            continue
    raise last_exception

A few operational patterns worth building around that core loop:

  1. Health-check your proxy pool on a schedule, not just when a request fails. Send a lightweight request to a known-fast endpoint every few minutes and pull dead proxies out of rotation before your scraper wastes retries on them.
  2. Isolate sessions per proxy when using connection pooling. A single Session object reused across many different proxies defeats some of the pooling benefit and can cause connection-state confusion under concurrency; create one session per proxy, or per worker thread, instead.
  3. Add exponential backoff to your retry logic, not just a flat retry count. A proxy that's temporarily rate-limited by the target site needs a delay, not an immediate hammering retry that gets your whole pool flagged.
  4. Respect the target site's actual rate limits rather than pushing concurrency until something breaks. Aggressive request rates are what get IP ranges blacklisted in bulk, taking out proxies that had nothing to do with the offending traffic.

Pro Tip: Building your own rotation, health-checking, and pool-management logic from scratch is a real engineering project, not a weekend script. If you're spending more time maintaining the rotation system than the scraper it's meant to support, that's usually the signal to offload it to a provider that handles rotation and health checks server-side.

Concurrency adds its own wrinkle: running dozens of threads or async tasks against the same proxy list without coordination means you can't easily tell which proxy caused which failure. A simple mapping of proxy to a rolling failure count, checked before each assignment, solves most of that without much extra code.

Troubleshooting Checklist for the Most Common Proxy Errors

Most Requests proxy failures fall into four buckets, and each one has a fast, specific diagnostic path rather than a vague "check your config" answer.

  • MissingSchema: Your proxy URL is missing http://, https://, or socks5:// in front of the host. Print your proxies dictionary right before the request call (print(proxies)) and confirm every value starts with a scheme.
  • ProxyError: Usually means the proxy server itself is unreachable. Test connectivity outside Python first: curl -x http://host:port https://httpbin.org/ip, or telnet host port to confirm the port is even open. If curl also fails, the proxy is down or your network is blocking the port, not a request bug.
  • SSLError: Check whether you're behind an intercepting proxy, then verify your CA bundle configuration. Setting REQUESTS_CA_BUNDLE or passing verify='/path/to/cacert.pem' resolves this in most cases, as covered above.
  • 407 Proxy Authentication Required: Verify your credentials are current and properly URL-encoded. Reproduce the exact request with curl -x http://user:pass@host:port to confirm whether the proxy or your code is the problem.
  • Environment variable conflicts: If a proxy setting seems to appear from nowhere, run import urllib.request; print(urllib.request.getproxies()) inside your script to see exactly what Requests is detecting from the environment, then override it explicitly with os.environ['NO_PROXY'] = '*' if you need a clean slate for testing.

Pro Tip: Keep a one-line debug function handy during development: a small wrapper that prints the resolved proxies dict, the request URL, and the response status code before and after every call. It turns a 20-minute guessing session into a 30-second diagnosis.

Best Practices for Secure, Reliable Proxy Use

Treat proxy credentials the same way you'd treat database passwords or API keys, because functionally, that's exactly what they are.

  • Never hard-code credentials in source files. Load them from environment variables, a .env file excluded from version control, or a dedicated secrets manager if you're running this in any shared or production environment.
  • Always set explicit timeouts on every request (requests.get(url, proxies=proxies, timeout=10)). A hung proxy connection without a timeout will stall your entire script indefinitely.
  • Track proxy health metrics over time, not just pass/fail on the current request. A proxy with a rising error rate over the last hour is worth pulling from rotation before it fails outright.
  • Respect the target site's rate limits and terms of service. A proxy changes your IP address; it doesn't change whether scraping a given site is permitted, and NatProxies's acceptable use policy is worth reading if you're unsure where those lines sit for your specific use case.
  • Automate decommissioning of unhealthy proxies rather than manually pruning a list. A scheduled health check that removes consistently failing IPs from your pool saves far more engineering time than it costs to build.

None of these are optional once a scraper moves from "personal script" to anything running unattended on a schedule, hitting real infrastructure, and representing your organization's traffic to the outside world.

When a Managed Proxy Provider Makes More Sense Than DIY

Building your own rotation logic, health checks, and geographic targeting works fine for a small project. It stops working once you need hundreds of concurrent sessions, consistent geographic targeting across a dozen countries, or authentication that doesn't break every time your provider updates its infrastructure.

That's the point where a managed service like NatProxies starts paying for itself. Dedicated static ISP proxies handle scenarios where you need a consistent IP tied to a single account over time, billed per IP rather than by bandwidth consumed. Rotating residential proxies, billed per GB with country, state, and city targeting, fit high-volume scraping and ad verification work where IP diversity matters more than IP stability.

When evaluating any proxy provider, three things matter more than marketing copy: what session types they actually support (sticky, rotating, or both), what authentication options they offer (credential-based versus IP whitelisting), and whether their bandwidth and pricing model is stated plainly rather than buried in fine print. A provider that's vague on any of those three is a provider you'll be debugging at 2 a.m.

What Most Proxy Guides Get Wrong About "It Just Works"

Most tutorials treat proxies as a solved problem the moment requests.get(url, proxies=proxies) returns a 200.

The projects that come to Natproxies with proxy problems almost never have a proxy problem in the strict sense. Often, they have an SSL interception issue because their corporate network sits in the middle of every connection, a 407 they've misdiagnosed as a bad proxy when it's actually a stale credential, or a geographic targeting requirement they tried to solve with a single static IP instead of city-level residential targeting. The Requests library itself is genuinely simple. What breaks is everything adjacent to it: environment variables silently overriding careful configuration, DNS leaking through socks5 instead of socks5h, or a rotation script that works fine at ten requests a minute and falls apart at a thousand.

The honest advice, and it's not the advice most guides give, is to stop treating proxy configuration as a one-time setup step. It's ongoing infrastructure, with its own failure modes and its own maintenance burden, and the earlier you accept that, the less time you'll spend debugging a "Requests problem" that was never about Requests at all.

— proxy

Get Started With NatProxies for Scraping and Automation

Proxy providers exist for the moment your proxy setup stops being a five-line dictionary and starts being infrastructure you need to trust. Instead of building rotation, health checks, and geographic targeting from scratch, you can get dedicated static ISP proxies billed per IP, rotating residential proxies billed per GB with geographic targeting, and mobile 4G/5G options, sometimes with unlimited bandwidth and instant checkout options so you're not waiting days for account approval.

Natproxies

If your current pain point is 407 errors or IP-based blocking, the ISP proxies product page breaks down authentication options and per-IP pricing directly. If you're scraping at volume across multiple regions, the rotating residential product page covers sticky versus rotating session behavior and geographic targeting granularity. Either way, the pricing page lays out the cost model before you commit to anything, so you can compare it against the engineering hours you'd spend maintaining a DIY rotation system. Start there, pick the proxy type that matches your session needs, and get a working setup running the same day.

Sources

FAQ

Are Proxy Servers Illegal?

Using a proxy server is legal in most jurisdictions; what you do through it, such as violating a website's terms of service or scraping data unlawfully, is what can create legal exposure, not the proxy itself.

What Are the Three Main Types of Proxies?

The three types most relevant to Requests users are dedicated ISP proxies (a static IP tied to your account), rotating residential proxies (IPs sourced from real residential connections that change per session or request), and mobile proxies (IPs from cellular carrier networks), each suited to different scraping and automation needs.

What Is the Difference Between a Proxy and an API?

A proxy routes your existing HTTP traffic through an intermediary IP address without changing the request's structure, while an API is a defined interface a service exposes for you to request specific data or actions in a structured format; you can use a proxy to reach an API, but they solve different problems.

What Does It Mean to "Proxy" a Request?

Proxying a request means routing it through an intermediary server that forwards it to the destination on your behalf, so the destination sees the proxy's IP address instead of your own.

Why Do I Get a MissingSchema Error With Requests Proxies?

MissingSchema means your proxy URL is missing its scheme prefix, such as http:// or socks5://; adding the correct prefix to every value in your proxies dictionary resolves it.