首頁
地區
網誌
產品
住宅代理 不限量住宅代理 靜態住宅代理 靜態數據中心代理 共享靜態代理
資源
套餐購買 地區 網誌 幫助中心 常見問題
註冊

Verifying Geo-Fencing Rules With Residential Proxies in Python

Verifying Geo-Fencing Rules With Residential Proxies in Python

Your geo-fencing rule says JP users can't access the premium library. But how did you verify that? Manually swapping to a VPN and clicking around doesn't count—and neither does testing from a datacenter proxy.

We hit this on a content platform that had Cloudflare sitting in front of the application. Every JP test case passed because Cloudflare's IP reputation system was flagging our datacenter ASN as hosting infrastructure, not consumer traffic, and bypassing the geo-fencing evaluation entirely before it even ran. The geo rule never executed. We found out three days after launch when a JP user filed a support ticket. The fix took 20 minutes. The debugging took two days.

That failure pattern is the reason this walkthrough uses residential proxies, verifies the exit country before every assertion, and then checks the actual application response. The two-step check—confirm exit country first, then hit the app—is how you separate a proxy misconfiguration from an actual policy failure. Along the way, we'll cover what a false positive looks like from either direction.

What You'll Need Before Running the Tests

You need Python 3.9+ and two packages: requests for HTTP calls and pytest for repeatable test cases. The Requests docs recommend a Session object when you want to persist settings across multiple requests, and the pytest docs cover @pytest.mark.parametrize as the built-in way to run one test function across many input combinations.

Install the basics:

python -m venv .venv
source .venv/bin/activate  # Windows: .venv\\Scripts\\activate
pip install requests pytest

You also need a residential proxy service that supports country-level geo-targeting. Whatever network you choose, it needs two things for this test suite to work correctly: country-level targeting and sticky sessions long enough to hold the same IP identity from the exit-country check to the application request. Proxy001's residential proxy service covers both—it offers country-level geo-targeting across 200+ countries, sticky sessions, and Python-oriented setup docs. Its 500MB free trial is enough to validate the full suite before committing to a plan.

Most residential proxy providers express geo-targeting as a country code embedded in the username, using a format like this:

http://username-country-US:password@gateway.provider.com:10000

The exact syntax varies by vendor—check your provider's dashboard or API docs and substitute the correct format into the examples below. Once you have the full URL for each country you want to test, store them as environment variables:

export GEO\_PROXY\_US="http://USERNAME-country-US:PASSWORD@gateway.provider.com:10000"
export GEO\_PROXY\_GB="http://USERNAME-country-GB:PASSWORD@gateway.provider.com:10000"
export GEO\_PROXY\_JP="http://USERNAME-country-JP:PASSWORD@gateway.provider.com:10000"
export GEO\_PROXY\_DE="http://USERNAME-country-DE:PASSWORD@gateway.provider.com:10000"

Putting each country's URL in a separate variable sounds slightly manual, but it eliminates the most common setup failure: using the wrong geo-targeting syntax and then wondering why the country check returns the wrong result.

Why Datacenter Proxies Give You False Results

Residential proxies route through IP space associated with real consumer devices and ISPs. Datacenter proxies come from hosting infrastructure. Most geo-fencing implementations sit at the edge—CDN or WAF—and those layers apply ASN reputation scoring before the geo check even runs. When an ASN is flagged as hosting infrastructure rather than a consumer ISP, some platforms short-circuit to a blanket 403 or silently bypass the geo logic entirely. That means a datacenter IP can produce either a false pass or a false block depending on how your specific edge policy is ordered—neither result tells you whether the geo rule actually works.

Don't optimize for speed first on this test suite. Optimize for test validity.

Step 1: Build a Country-Targeted Proxy Session

Requests supports proxy configuration through a Session, and sessions persist settings across requests by design. That persistence is what makes a session the right abstraction here: one test case needs to perform both an exit-country check and an application request through the same proxy identity.

Create geo\_helpers.py:

import os
import requests

DEFAULT\_TIMEOUT = 20

def get\_proxy\_url(country\_code: str) -> str:
    env\_name = f"GEO\_PROXY\_{country\_code.upper()}"
    proxy\_url = os.getenv(env\_name)
    if not proxy\_url:
        raise RuntimeError(
            f"Missing {env\_name}. Add a fully formed proxy URL for that country."
        )
    return proxy\_url

def build\_session(country\_code: str) -> requests.Session:
    proxy\_url = get\_proxy\_url(country\_code)
    session = requests.Session()
    session.proxies.update({
        "http": proxy\_url,
        "https": proxy\_url,
    })
    session.headers.update({
        "User-Agent": (
            "Mozilla/5.0 (Macintosh; Intel Mac OS X 10\_15\_7) "
            "AppleWebKit/537.36 (KHTML, like Gecko) "
            "Chrome/123.0.0.0 Safari/537.36"
        )
    })
    return session

def get(url: str, session: requests.Session, \*\*kwargs) -> requests.Response:
    timeout = kwargs.pop("timeout", DEFAULT\_TIMEOUT)
    return session.get(url, timeout=timeout, allow\_redirects=True, \*\*kwargs)

Wrapping session.get() in your own helper gives you one place to standardize timeouts, headers, retries, and logging. Geo-fencing tests fail for operational reasons far more often than they fail for policy reasons—reducing variation early saves time later.

Step 2: Confirm Your Exit IP Is in the Right Country

Before touching the application endpoint, confirm the request is actually leaving from the intended country. httpbin.org exposes a /ip endpoint that returns the requester's visible IP, and ipinfo.io maps that IP to a country code. Using both in sequence gives you the raw IP and the classified location.

Add these helpers to geo\_helpers.py:

def get\_exit\_ip(session: requests.Session) -> str:
    r = get("https://httpbin.org/ip", session)
    r.raise\_for\_status()
    data = r.json()
    return data\["origin"].split(",")\[0].strip()

def get\_exit\_country(session: requests.Session) -> str:
    r = get("https://ipinfo.io/json", session)
    r.raise\_for\_status()
    data = r.json()
    country = data.get("country")
    if not country:
        raise RuntimeError(
            f"Could not determine exit country from ipinfo response: {data}"
        )
    return country.upper()

Then add a quick smoke-test script:

if \_\_name\_\_ == "\_\_main\_\_":
    for country in ["US", "GB", "JP", "DE"]:
        session = build\_session(country)
        ip = get\_exit\_ip(session)
        country\_result = get\_exit\_country(session)
        print(f"{country}: {ip} ({country\_result})")
        assert country == country\_result, (
            f"Proxy country mismatch: requested={country} actual={country\_result}"
        )

Run it once before building your full suite:

python geo\_helpers.py

The output should show the same country code in both the request parameter and the country_result column. If they don't match, your proxy URL or geo-targeting syntax is wrong. Fix it before moving to Step 3.

Step 3: Classify Allow vs. Block Responses

Your application returns different status codes or page content depending on whether a region is allowed. You need to classify those responses consistently. Add a classification helper to geo\_helpers.py:

def looks\_like\_security\_challenge(response: requests.Response) -> bool:
    """
    Detect bot-screening or captcha challenges.
    These indicate a pre-geo security layer, not a geo-policy failure.
    """
    challenge\_markers = [
        "challenge",
        "captcha",
        "bot check",
        "please wait",
        "javascript required",
        "cloudflare",
    ]
    content\_lower = response.text.lower()
    return any(marker in content\_lower for marker in challenge\_markers)

def looks\_like\_allowed(response: requests.Response, marker: str) -> bool:
    """
    Check whether the response contains content that marks access as allowed.
    """
    return marker.lower() in response.text.lower()

def looks\_like\_blocked(response: requests.Response, block\_status: int, block\_markers: list\[str\], block\_path\_prefixes: list\[str\]) -> bool:
    """
    Check whether the response indicates the region is blocked.
    Block can signal via: status code, page text, or redirect path prefix.
    """
    # Check status code
    if response.status_code == block\_status:
        return True
    
    # Check for block page markers
    content\_lower = response.text.lower()
    if any(marker.lower() in content\_lower for marker in block\_markers):
        return True
    
    # Check for redirect to a block path
    final\_url = response.url.lower()
    if any(final\_url.startswith(prefix.lower()) for prefix in block\_path\_prefixes):
        return True
    
    return False

Keep markers tied to your actual application content—don't guess. If your app redirects to /blocked and returns a 451 status, set both. If it serves a 403 with the literal text "Access Denied", set that. Be specific.

Step 4: Build the Parametrized Test

Create test_geo_fencing.py:

import pytest
from geo_helpers import (
    build_session, get_exit_country, get, 
    looks_like_security_challenge, looks_like_allowed, looks_like_blocked
)

# Configuration: customize these for your application
APP_URL = "https://your-app.example.com/premium-video"
ALLOW_MARKER = "premium content"  # Text visible only when access is granted
BLOCK_STATUS = 451                 # HTTP status for blocked regions (or use 403)
BLOCK_MARKERS = ["access denied", "geo-restricted"]  # Block page text
BLOCK_PATH_PREFIXES = ["https://your-app.example.com/blocked"]  # Redirect URLs

# Test matrix: (access_rule, country_code)
TEST_CASES = [
    ("allow", "US"),
    ("allow", "GB"),
    ("block", "JP"),
    ("block", "DE"),
]

@pytest.mark.parametrize("rule,country", TEST_CASES, ids=lambda x: f"{x[0]}-{x[1]}")
def test_geo_fencing(rule, country):
    """
    Verify geo-fencing policy by:
    1. Confirming the proxy exit country matches the requested country
    2. Checking the application response against the expected rule
    """
    # Build a session with the geo-targeted proxy
    session = build_session(country)
    
    # Verify the proxy is exiting from the right country
    actual_country = get_exit_country(session)
    assert actual_country == country, (
        f"Proxy country mismatch: requested={country} actual={actual_country}"
    )
    
    # Request the application endpoint
    response = get(APP_URL, session)
    
    # Fail early if we hit a security challenge (pre-geo layer)
    assert not looks_like_security_challenge(response), (
        f"Security challenge detected from {country} (status={response.status_code})"
    )
    
    # Enforce the geo-policy rule
    if rule == "allow":
        assert response.status_code == 200, (
            f"Expected 200 for allowed region {country}, got {response.status_code}"
        )
        assert looks_like_allowed(response, ALLOW_MARKER), (
            f"Allow marker '{ALLOW_MARKER}' not found in response from {country}"
        )
    elif rule == "block":
        assert looks_like_blocked(response, BLOCK_STATUS, BLOCK_MARKERS, BLOCK_PATH_PREFIXES), (
            f"Expected request from {country} to be blocked, "
            f"but got status={response.status_code} url={response.url}"
        )

Notes:

  • Keep ALLOW_MARKER tied to the premium feature—a section header, a specific media ID, or something that only appears when access is granted. Don't use generic text like "welcome" or "content".

  • Keep BLOCK_MARKERS tied to your own block page copy, not guesses about what a block page might say. If your app team hasn't defined stable block-page text yet, ask them to add it—this is a testability issue, not just a testing-convenience issue.

Step 5: Run the Tests and Interpret Results

Run the suite:

pytest -v test\_geo\_fencing.py

A healthy run looks like this:

test\_geo\_fencing.py::test\_geo\_fencing\[allow-us] PASSED
test\_geo\_fencing.py::test\_geo\_fencing\[allow-gb] PASSED
test\_geo\_fencing.py::test\_geo\_fencing\[block-jp] PASSED
test\_geo\_fencing.py::test\_geo\_fencing\[block-de] PASSED

Failures are explicit about which layer broke:

# Network-input failure
AssertionError: Proxy country mismatch: requested=JP actual=SG

# Policy failure
AssertionError: Expected request from DE to be blocked,
                but got status=200 url=https://your-app.example.com/premium-video

# Pre-geo security layer
AssertionError: Security challenge detected from JP (status=403)

Those three failure types have different owners and different fixes. Don't conflate them.

For CI, store your proxy URLs as secrets and trigger the suite on deploy rather than every commit:

name: geo-fencing-check

on:
  workflow\_dispatch:
  push:
    branches: \[ main ]

jobs:
  test-geo:
    runs-on: ubuntu-latest
    env:
      GEO\_PROXY\_US: ${{ secrets.GEO\_PROXY\_US }}
      GEO\_PROXY\_GB: ${{ secrets.GEO\_PROXY\_GB }}
      GEO\_PROXY\_JP: ${{ secrets.GEO\_PROXY\_JP }}
      GEO\_PROXY\_DE: ${{ secrets.GEO\_PROXY\_DE }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.11"
      - run: pip install requests pytest
      - run: pytest -v test\_geo\_fencing.py

Log the requested country, actual exit country, status code, final URL, and test timestamp for every run. That record is how you distinguish "the geo rule regressed" from "the proxy endpoint changed" when something breaks three weeks from now.

Troubleshooting: Why Your Tests Might Fail

1) Proxy country mismatch: requested=JP actual=SG

The proxy URL for that country isn't working as expected. We've hit this most often in two scenarios: the provider changed their session-gateway hostname without updating the dashboard, or the country code syntax in the username was slightly wrong (some providers use country\_JP, others use country-JP, others use countryCode-jp). The fix is to check your provider's current API docs directly rather than borrowing a URL format from a different provider's example. The exact targeting syntax is vendor-specific and isn't standardized across the industry.

2) The app returns a security challenge page instead of a geo result

You hit a bot-screening layer before geo-fencing ran—the same failure pattern from the intro. The test framework will catch this with looks\_like\_security\_challenge() and fail the assertion explicitly rather than letting it pass through as a false result.

Real browsers send a full set of request headers alongside their User-AgentAccept-Language, Accept-Encoding, Referer, and others. If your test session is missing several of these, it may not reflect the same profile as genuine residential traffic originating from a real device. Check your provider's documentation on whether they recommend supplementing headers to better match the behavior of real residential users in your target markets, and add any recommended headers to build\_session().

3) The blocked case returns 200 OK and the test fails

This means your block classification isn't matching the actual block signal your app sends. Check three things in order: first, does your app actually return a dedicated block page, or does it silently serve the homepage with a 200? Second, is the block page text in BLOCK\_MARKERS an exact substring of what the page returns (case-insensitive)? Third, if the app redirects to a block URL, is that path prefix in BLOCK\_PATH\_PREFIXES? We've seen this most often when the app team changes the block-page copy after the tests were written—the solution is to make block-page content a contract, not an implementation detail.

4) The allow case passes the status check but fails on the content marker

Your ALLOW\_MARKER is too fragile or has drifted from the actual page content. Pick a string that's tied to the specific gated feature—a media ID, a section header unique to the premium tier, or a stable element that appears only when the user is in an allowed region. Avoid strings that appear site-wide. We've also hit this when A/B test variants changed the page copy: if you have active experiments running, check whether the marker is present in all variants.

5) Tests pass locally but fail in CI

This is almost always a secrets or network egress problem, not a geo-fencing problem. Confirm in this order: first, that your CI job actually has the proxy URL secrets (GEO\_PROXY\_\*) and they're not empty; second, that the runner allows outbound HTTPS. Add a connectivity check step to your workflow to isolate this before debugging anything else:

- name: Verify proxy connectivity
  run: |
    curl -v --proxy "$GEO\_PROXY\_US" https://ipinfo.io/json

If that step fails, the problem is runner egress or secret misconfiguration. If it succeeds but the pytest run still fails, the issue is in the test code itself.

Compliance Note: Cost and Legal Considerations

Use this pattern to test your own application or services you're authorized to validate. Geo-fencing is a standard compliance and licensing mechanism—verifying that your own rules work correctly is a defensible engineering practice with no gray area.

On cost: each IP-check call returns a small JSON payload, well under a kilobyte. Your gated application page will be the dominant cost, and even a multi-hundred-kilobyte HTML response means a complete four-country suite stays in the low-megabyte range at most. At residential proxy rates, the bandwidth cost per full run is a fraction of a cent. That said, run this suite before deploys or on a scheduled cadence for your critical markets rather than on every commit. The cost per run is negligible, but running it more frequently than needed adds noise to your CI history without adding signal. Whatever residential proxy network you use, confirm it supports country-level targeting and sticky sessions before building your test matrix against it.

---

The cleanest implementation is also the one teams actually maintain: one test file, one helper for exit-country verification, one helper for allow/block classification, and an explicit country matrix. Once that's in place, geo-fencing stops being a manual spot check you run the day before launch and becomes a regression test that runs before every deploy.

---

Ready to run this? Proxy001's residential proxy service is built around the exact requirements this workflow depends on: country-level geo-targeting across 200+ countries, sticky sessions for stable multi-request test sequences, and Python-ready setup docs. There's a 500MB free trial that's enough to validate your full test matrix—no enterprise contract needed to get started. Once you've confirmed the suite works, its pay-as-you-go pricing ($2/GB, scaling down to $0.70/GB at volume) means the ongoing CI cost stays well within a test-infrastructure budget. Start with the free trial and run the suite against your staging environment before your next deploy.

開啟您安全穩定的
全球代理服務
幾分鐘內即可開始使用,充分釋放代理的潛力。
立即開始