Skip to content

Commit ed6f3a0

Browse files
committed
Fix: Respect no_proxy in proxies dictionary and NO_PROXY env var
This commit addresses issue #5000 by ensuring that the `no_proxy` directive is respected, whether it's provided directly within the `proxies` dictionary passed to request functions or set as the `NO_PROXY` environment variable. Previously, `no_proxy` in the `proxies` dictionary was not fully honored, and the `NO_PROXY` environment variable was not always checked when a `proxies` dictionary was provided. This change includes: 1. **`src/requests/sessions.py`**: Modified the `Session.send` method to check the `no_proxy` key within the `kwargs['proxies']` dictionary. If the request URL matches any pattern in the `no_proxy` list, the proxies are cleared for that request. 2. **`src/requests/utils.py`**: Updated the `select_proxy` function to check the `NO_PROXY` environment variable using `should_bypass_proxies` before selecting a proxy from the provided `proxies` dictionary. 3. **`tests/test_requests.py`**: Added new test cases (`test_no_proxy_in_proxies_dict`, `test_no_proxy_star_in_proxies_dict`, `test_no_proxy_not_matching_in_proxies_dict`) to verify that `no_proxy` within the `proxies` dictionary works as expected, using mocks to check if the proxy is bypassed. 4. **`tests/test_utils.py`**: Added new test cases (`test_select_proxy_with_no_proxy`) to ensure the `NO_PROXY` environment variable is correctly handled by `select_proxy`. These changes ensure consistent behavior for proxy bypass logic, regardless of how the proxy settings are configured. Closes #5000
1 parent 7029833 commit ed6f3a0

File tree

4 files changed

+102
-1
lines changed

4 files changed

+102
-1
lines changed

src/requests/sessions.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -699,12 +699,17 @@ def send(self, request, **kwargs):
699699
# Start time (approximately) of the request
700700
start = preferred_clock()
701701

702+
# Check if the URL should bypass the proxy based on 'no_proxy' in kwargs
703+
if kwargs.get("proxies"):
704+
no_proxy_list = kwargs["proxies"].get("no_proxy")
705+
if should_bypass_proxies(request.url, no_proxy=no_proxy_list):
706+
kwargs["proxies"] = {} # Clear proxies if URL should be bypassed
707+
702708
# Send the request
703709
r = adapter.send(request, **kwargs)
704710

705711
# Total elapsed time of the request (approximately)
706712
elapsed = preferred_clock() - start
707-
r.elapsed = timedelta(seconds=elapsed)
708713

709714
# Response manipulation hooks
710715
r = dispatch_hook("response", hooks, r, **kwargs)

src/requests/utils.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -836,6 +836,10 @@ def select_proxy(url, proxies):
836836
if urlparts.hostname is None:
837837
return proxies.get(urlparts.scheme, proxies.get("all"))
838838

839+
# Check NO_PROXY from environment first
840+
if should_bypass_proxies(url, no_proxy=os.environ.get('NO_PROXY')):
841+
return None
842+
839843
proxy_keys = [
840844
urlparts.scheme + "://" + urlparts.hostname,
841845
urlparts.scheme,

tests/test_requests.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1997,7 +1997,71 @@ def test_rewind_partially_read_body(self):
19971997
requests.utils.rewind_body(prep)
19981998
assert prep.body.read() == b"data"
19991999

2000+
20002001
def test_rewind_body_no_seek(self):
2002+
pass
2003+
2004+
def test_no_proxy_in_proxies_dict(self, httpbin):
2005+
# Test case 1: 'no_proxy' in proxies dict
2006+
proxies = {"https": "https://b.r.o.k.e.n.com", "no_proxy": "google.com"}
2007+
with mock.patch('requests.adapters.HTTPAdapter.send') as mock_send:
2008+
mock_response = mock.Mock()
2009+
mock_response.url = "https://google.com"
2010+
mock_response.headers = {}
2011+
mock_response.status_code = 200
2012+
mock_response.is_redirect = False
2013+
mock_response.history = []
2014+
mock_send.return_value = mock_response
2015+
requests.get("https://google.com", proxies=proxies)
2016+
# Check if proxies were cleared before sending
2017+
args, kwargs = mock_send.call_args
2018+
assert not kwargs.get("proxies")
2019+
2020+
# Test case 2: 'no_proxy' in proxies dict with env var
2021+
proxies = {"https": "https://b.r.o.k.e.n.com"}
2022+
with override_environ(NO_PROXY="google.com"):
2023+
with mock.patch('requests.adapters.HTTPAdapter.send') as mock_send:
2024+
mock_response = mock.Mock()
2025+
mock_response.url = "https://google.com"
2026+
mock_response.headers = {}
2027+
mock_response.status_code = 200
2028+
mock_response.is_redirect = False
2029+
mock_response.history = []
2030+
mock_send.return_value = mock_response
2031+
requests.get("https://google.com", proxies=proxies)
2032+
# Check if proxies were cleared before sending
2033+
args, kwargs = mock_send.call_args
2034+
assert not kwargs.get("proxies")
2035+
2036+
def test_no_proxy_star_in_proxies_dict(self, httpbin):
2037+
proxies = {"https": "https://b.r.o.k.e.n.com", "no_proxy": "*"}
2038+
with mock.patch('requests.adapters.HTTPAdapter.send') as mock_send:
2039+
mock_response = mock.Mock()
2040+
mock_response.url = "https://google.com"
2041+
mock_response.headers = {}
2042+
mock_response.status_code = 200
2043+
mock_response.is_redirect = False
2044+
mock_response.history = []
2045+
mock_send.return_value = mock_response
2046+
requests.get("https://google.com", proxies=proxies)
2047+
# Check if proxies were cleared before sending
2048+
args, kwargs = mock_send.call_args
2049+
assert not kwargs.get("proxies")
2050+
2051+
def test_no_proxy_not_matching_in_proxies_dict(self, httpbin):
2052+
proxies = {"https": "https://b.r.o.k.e.n.com", "no_proxy": "example.com"}
2053+
with mock.patch('requests.adapters.HTTPAdapter.send') as mock_send:
2054+
mock_response = mock.Mock()
2055+
mock_response.url = "https://google.com"
2056+
mock_response.headers = {}
2057+
mock_response.status_code = 200
2058+
mock_response.is_redirect = False
2059+
mock_response.history = []
2060+
mock_send.return_value = mock_response
2061+
requests.get("https://google.com", proxies=proxies)
2062+
# Check if proxies were NOT cleared before sending
2063+
args, kwargs = mock_send.call_args
2064+
assert kwargs.get("proxies") == proxies
20012065
class BadFileObj:
20022066
def __init__(self, data):
20032067
self.data = data

tests/test_utils.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -975,3 +975,31 @@ def QueryValueEx(key, value_name):
975975
monkeypatch.setattr(winreg, "OpenKey", OpenKey)
976976
monkeypatch.setattr(winreg, "QueryValueEx", QueryValueEx)
977977
assert should_bypass_proxies("http://example.com/", None) is False
978+
979+
980+
981+
@pytest.mark.parametrize(
982+
"url, no_proxy_env, proxies, expected_proxy",
983+
[
984+
("http://example.com", "example.com", {"http": "http://proxy.com"}, None),
985+
("http://example.com", "other.com", {"http": "http://proxy.com"}, "http://proxy.com"),
986+
("http://test.example.com", "example.com", {"http": "http://proxy.com"}, None),
987+
("http://example.com", "*", {"http": "http://proxy.com"}, None),
988+
("http://example.com", None, {"http": "http://proxy.com"}, "http://proxy.com"),
989+
("http://internal.com", "internal.com", {"all": "http://proxy.com"}, None),
990+
("https://internal.com", "internal.com", {"all": "http://proxy.com"}, None),
991+
],
992+
)
993+
def test_select_proxy_with_no_proxy(
994+
url, no_proxy_env, proxies, expected_proxy, monkeypatch
995+
):
996+
"""Test that select_proxy honors NO_PROXY environment variable."""
997+
if no_proxy_env:
998+
monkeypatch.setenv("NO_PROXY", no_proxy_env)
999+
else:
1000+
monkeypatch.delenv("NO_PROXY", raising=False)
1001+
1002+
assert select_proxy(url, proxies) == expected_proxy
1003+
1004+
# EOF Marker
1005+

0 commit comments

Comments
 (0)