Skip to content

Commit 9c9be36

Browse files
SNOW-2463378: no_proxy support (#2596)
1 parent 6f48c9e commit 9c9be36

File tree

19 files changed

+760
-130
lines changed

19 files changed

+760
-130
lines changed

DESCRIPTION.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Source code is also available at: https://github.com/snowflakedb/snowflake-conne
1212
- This allows headless environments (like Docker or Airflow) running locally to auth via a browser URL.
1313
- Fix compilation error when building from sources with libc++.
1414
- Pin lower versions of dependencies to oldest version without vulnerabilities.
15+
- Added no_proxy parameter for proxy configuration without using environmental variables.
1516

1617
- v4.0.0(October 09,2025)
1718
- Added support for checking certificates revocation using revocation lists (CRLs)

src/snowflake/connector/auth/_auth.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
from ..platform_detection import detect_platforms
5656
from ..session_manager import BaseHttpConfig, HttpConfig
5757
from ..session_manager import SessionManager as SyncSessionManager
58+
from ..session_manager import SessionManagerFactory
5859
from ..sqlstate import SQLSTATE_CONNECTION_WAS_NOT_ESTABLISHED
5960
from ..token_cache import TokenCache, TokenKey, TokenType
6061
from ..version import VERSION
@@ -116,7 +117,7 @@ def base_auth_data(
116117
# Extract base fields (automatically excludes subclass-specific fields)
117118
# Note: It won't be possible to pass adapter_factory from outer async-code to this part of code
118119
sync_config = HttpConfig(**http_config.to_base_dict())
119-
session_manager = SyncSessionManager(config=sync_config)
120+
session_manager = SessionManagerFactory.get_manager(config=sync_config)
120121

121122
return {
122123
"data": {

src/snowflake/connector/connection.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,12 @@
129129
ReauthenticationRequest,
130130
SnowflakeRestful,
131131
)
132-
from .session_manager import HttpConfig, ProxySupportAdapterFactory, SessionManager
132+
from .session_manager import (
133+
HttpConfig,
134+
ProxySupportAdapterFactory,
135+
SessionManager,
136+
SessionManagerFactory,
137+
)
133138
from .sqlstate import SQLSTATE_CONNECTION_NOT_EXISTS, SQLSTATE_FEATURE_NOT_SUPPORTED
134139
from .telemetry import TelemetryClient, TelemetryData, TelemetryField
135140
from .time_util import HeartBeatTimer, get_time_millis
@@ -199,6 +204,10 @@ def _get_private_bytes_from_file(
199204
"proxy_port": (None, (type(None), str)), # snowflake
200205
"proxy_user": (None, (type(None), str)), # snowflake
201206
"proxy_password": (None, (type(None), str)), # snowflake
207+
"no_proxy": (
208+
None,
209+
(type(None), str, Iterable),
210+
), # hosts/ips to bypass proxy (str or iterable)
202211
"protocol": ("https", str), # snowflake
203212
"warehouse": (None, (type(None), str)), # snowflake
204213
"region": (None, (type(None), str)), # snowflake
@@ -808,6 +817,10 @@ def proxy_user(self) -> str | None:
808817
def proxy_password(self) -> str | None:
809818
return self._proxy_password
810819

820+
@property
821+
def no_proxy(self) -> str | Iterable | None:
822+
return self._no_proxy
823+
811824
@property
812825
def account(self) -> str:
813826
return self._account
@@ -1076,8 +1089,17 @@ def connect(self, **kwargs) -> None:
10761089
proxy_port=self.proxy_port,
10771090
proxy_user=self.proxy_user,
10781091
proxy_password=self.proxy_password,
1092+
no_proxy=(
1093+
",".join(str(x) for x in self.no_proxy)
1094+
if (
1095+
self.no_proxy is not None
1096+
and isinstance(self.no_proxy, Iterable)
1097+
and not isinstance(self.no_proxy, (str, bytes))
1098+
)
1099+
else self.no_proxy
1100+
),
10791101
)
1080-
self._session_manager = SessionManager(self._http_config)
1102+
self._session_manager = SessionManagerFactory.get_manager(self._http_config)
10811103

10821104
if self.enable_connection_diag:
10831105
exceptions_dict = {}

src/snowflake/connector/connection_diagnostic.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

2020
from .compat import IS_WINDOWS, urlparse
2121
from .cursor import SnowflakeCursor
22-
from .session_manager import SessionManager
22+
from .session_manager import SessionManager, SessionManagerFactory
2323
from .url_util import extract_top_level_domain_from_hostname
2424
from .vendored import urllib3
2525

@@ -197,7 +197,7 @@ def __init__(
197197
self._session_manager = (
198198
session_manager.clone(use_pooling=False)
199199
if session_manager
200-
else SessionManager(use_pooling=False)
200+
else SessionManagerFactory.get_manager(use_pooling=False)
201201
)
202202

203203
def __parse_proxy(self, proxy_url: str) -> tuple[str, str, str, str]:

src/snowflake/connector/network.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,12 @@
8181
ServiceUnavailableError,
8282
TooManyRequests,
8383
)
84-
from .session_manager import ProxySupportAdapterFactory, SessionManager, SessionPool
84+
from .session_manager import (
85+
ProxySupportAdapterFactory,
86+
SessionManager,
87+
SessionManagerFactory,
88+
SessionPool,
89+
)
8590
from .sqlstate import (
8691
SQLSTATE_CONNECTION_NOT_EXISTS,
8792
SQLSTATE_CONNECTION_REJECTED,
@@ -324,7 +329,9 @@ def __init__(
324329
session_manager = (
325330
connection._session_manager
326331
if (connection and connection._session_manager)
327-
else SessionManager(adapter_factory=ProxySupportAdapterFactory())
332+
else SessionManagerFactory.get_manager(
333+
adapter_factory=ProxySupportAdapterFactory()
334+
)
328335
)
329336
self._session_manager = session_manager
330337
self._lock_token = Lock()
@@ -1213,5 +1220,5 @@ def _request_exec(
12131220
except Exception as err:
12141221
raise err
12151222

1216-
def use_session(self, url=None) -> Generator[Session, Any, None]:
1223+
def use_session(self, url: str | bytes) -> Generator[Session, Any, None]:
12171224
return self.session_manager.use_session(url)

src/snowflake/connector/ocsp_snowflake.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
from .backoff_policies import exponential_backoff
6060
from .cache import CacheEntry, SFDictCache, SFDictFileCache
6161
from .constants import OCSP_ROOT_CERTS_DICT_LOCK_TIMEOUT_DEFAULT_NO_TIMEOUT
62+
from .session_manager import SessionManagerFactory
6263
from .telemetry import TelemetryField, generate_telemetry_data_dict
6364
from .url_util import extract_top_level_domain_from_hostname, url_encode_str
6465
from .util_text import _base64_bytes_to_str
@@ -551,8 +552,8 @@ def _download_ocsp_response_cache(ocsp, url, do_retry: bool = True) -> bool:
551552
# Obtain SessionManager from ssl_wrap_socket context var if available
552553
session_manager = get_current_session_manager(
553554
use_pooling=False
554-
) or SessionManager(use_pooling=False)
555-
with session_manager.use_session() as session:
555+
) or SessionManagerFactory.get_manager(use_pooling=False)
556+
with session_manager.use_session(url) as session:
556557
max_retry = SnowflakeOCSP.OCSP_CACHE_SERVER_MAX_RETRY if do_retry else 1
557558
sleep_time = 1
558559
backoff = exponential_backoff()()
@@ -1646,9 +1647,9 @@ def _fetch_ocsp_response(
16461647
session_manager: SessionManager = (
16471648
context_session_manager
16481649
if context_session_manager is not None
1649-
else SessionManager(use_pooling=False)
1650+
else SessionManagerFactory.get_manager(use_pooling=False)
16501651
)
1651-
with session_manager.use_session() as session:
1652+
with session_manager.use_session(target_url) as session:
16521653
max_retry = sf_max_retry if do_retry else 1
16531654
sleep_time = 1
16541655
backoff = exponential_backoff()()

src/snowflake/connector/platform_detection.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
Config = botocore.config.Config
1414
IMDSFetcher = botocore.utils.IMDSFetcher
1515

16-
from .session_manager import SessionManager
16+
from .session_manager import SessionManager, SessionManagerFactory
1717
from .vendored.requests import RequestException, Timeout
1818

1919
logger = logging.getLogger(__name__)
@@ -415,7 +415,9 @@ def detect_platforms(
415415
logger.debug(
416416
"No session manager provided. HTTP settings may not be preserved. Using default."
417417
)
418-
session_manager = SessionManager(use_pooling=False, max_retries=0)
418+
session_manager = SessionManagerFactory.get_manager(
419+
use_pooling=False, max_retries=0
420+
)
419421

420422
# Run environment-only checks synchronously (no network calls, no threading overhead)
421423
platforms = {

src/snowflake/connector/result_batch.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
from .options import installed_pandas
2727
from .options import pyarrow as pa
2828
from .secret_detector import SecretDetector
29-
from .session_manager import HttpConfig, SessionManager
29+
from .session_manager import HttpConfig, SessionManager, SessionManagerFactory
3030
from .time_util import TimerContextManager
3131

3232
logger = getLogger(__name__)
@@ -319,7 +319,7 @@ def http_config(self, config: HttpConfig) -> None:
319319
if self._session_manager:
320320
self._session_manager.config = config
321321
else:
322-
self._session_manager = SessionManager(config=config)
322+
self._session_manager = SessionManagerFactory.get_manager(config=config)
323323

324324
def __iter__(
325325
self,
@@ -360,21 +360,27 @@ def _download(
360360
and connection.rest.session_manager is not None
361361
):
362362
# If connection was explicitly passed and not closed yet - we can reuse SessionManager with session pooling
363-
with connection.rest.use_session() as session:
363+
with connection.rest.use_session(
364+
request_data["url"]
365+
) as session:
364366
logger.debug(
365367
f"downloading result batch id: {self.id} with existing session {session}"
366368
)
367369
response = session.request("get", **request_data)
368370
elif self._session_manager is not None:
369371
# If connection is not accessible or was already closed, but cursors are now used to fetch the data - we will only reuse the http setup (through cloned SessionManager without session pooling)
370-
with self._session_manager.use_session() as session:
372+
with self._session_manager.use_session(
373+
request_data["url"]
374+
) as session:
371375
response = session.request("get", **request_data)
372376
else:
373377
# If there was no session manager cloned, then we are using a default Session Manager setup, since it is very unlikely to enter this part outside of testing
374378
logger.debug(
375379
f"downloading result batch id: {self.id} with new session through local session manager"
376380
)
377-
local_session_manager = SessionManager(use_pooling=False)
381+
local_session_manager = SessionManagerFactory.get_manager(
382+
use_pooling=False
383+
)
378384
response = local_session_manager.get(**request_data)
379385

380386
if response.status_code == OK:

src/snowflake/connector/session_manager.py

Lines changed: 72 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ class BaseHttpConfig:
134134
proxy_port: str | None = None
135135
proxy_user: str | None = None
136136
proxy_password: str | None = None
137+
no_proxy: str | None = None
137138

138139
def copy_with(self, **overrides: Any) -> BaseHttpConfig:
139140
"""Return a new config with overrides applied."""
@@ -191,12 +192,12 @@ def __init__(self, manager: SessionManager) -> None:
191192
self._active_sessions: set[SessionT] = set()
192193
self._manager = manager
193194

194-
def get_session(self) -> SessionT:
195+
def get_session(self, *, url: str | None = None) -> SessionT:
195196
"""Returns a session from the session pool or creates a new one."""
196197
try:
197198
session = self._idle_sessions.pop()
198199
except IndexError:
199-
session = self._manager.make_session()
200+
session = self._manager.make_session(url=url)
200201
self._active_sessions.add(session)
201202
return session
202203

@@ -403,6 +404,7 @@ def __init__(self, config: HttpConfig | None = None, **http_config_kwargs) -> No
403404
logger.debug("Creating a config for the SessionManager")
404405
config = HttpConfig(**http_config_kwargs)
405406
self._cfg: HttpConfig = config
407+
# Maps hostname to SessionPool instance for its connections
406408
self._sessions_map: dict[str | None, SessionPool] = collections.defaultdict(
407409
lambda: SessionPool(self)
408410
)
@@ -474,32 +476,50 @@ def _mount_adapters(self, session: requests.Session) -> None:
474476
)
475477
return
476478

477-
def make_session(self) -> Session:
479+
def make_session(self, *, url: str | None = None) -> Session:
478480
session = requests.Session()
479481
self._mount_adapters(session)
480-
session.proxies = {"http": self.proxy_url, "https": self.proxy_url}
481482
return session
482483

483484
@contextlib.contextmanager
484485
@_propagate_session_manager_to_ocsp
485486
def use_session(
486-
self, url: str | bytes | None = None, use_pooling: bool | None = None
487+
self, url: str | bytes, use_pooling: bool | None = None
487488
) -> Generator[Session, Any, None]:
489+
"""
490+
'url' is an obligatory parameter due to the need for correct proxy handling (i.e. bypassing caused by no_proxy settings).
491+
"""
488492
use_pooling = use_pooling if use_pooling is not None else self.use_pooling
489493
if not use_pooling:
490-
session = self.make_session()
494+
session = self.make_session(url=url)
491495
try:
492496
yield session
493497
finally:
494498
session.close()
495499
else:
496-
hostname = urlparse(url).hostname if url else None
497-
pool = self._sessions_map[hostname]
498-
session = pool.get_session()
499-
try:
500-
yield session
501-
finally:
502-
pool.return_session(session)
500+
yield from self._yield_session_from_pool(url)
501+
502+
def _yield_session_from_pool(
503+
self, url: str | bytes
504+
) -> Generator[SessionT, Any, None]:
505+
hostname = self._get_pooling_key_from_url(url)
506+
pool = self._sessions_map[hostname]
507+
session = pool.get_session(url=url)
508+
try:
509+
yield session
510+
finally:
511+
pool.return_session(session)
512+
513+
@staticmethod
514+
def _get_pooling_key_from_url(url: str) -> str | None:
515+
"""
516+
Derive the session pooling key (hostname) from a URL.
517+
518+
:param url: Absolute URL the session will be used for.
519+
:return: Hostname string or None if URL is missing/invalid.
520+
"""
521+
hostname = urlparse(url).hostname if url else None
522+
return hostname
503523

504524
def request(
505525
self,
@@ -586,3 +606,42 @@ def request(
586606
use_pooling=use_pooling,
587607
**kwargs,
588608
)
609+
610+
611+
class ProxySessionManager(SessionManager):
612+
def make_session(self, *, url: str | None = None) -> Session:
613+
session = requests.Session()
614+
self._mount_adapters(session)
615+
proxies = (
616+
{
617+
"no_proxy": self._cfg.no_proxy,
618+
}
619+
if requests.utils.should_bypass_proxies(url, no_proxy=self.config.no_proxy)
620+
else {
621+
"http": self.proxy_url,
622+
"https": self.proxy_url,
623+
"no_proxy": self.config.no_proxy,
624+
}
625+
)
626+
session.proxies = proxies
627+
return session
628+
629+
def clone(
630+
self,
631+
**http_config_overrides,
632+
) -> SessionManager:
633+
return ProxySessionManager.from_config(self._cfg, **http_config_overrides)
634+
635+
636+
class SessionManagerFactory:
637+
@staticmethod
638+
def get_manager(
639+
config: HttpConfig | None = None, **http_config_kwargs
640+
) -> SessionManager:
641+
has_param_proxies = (
642+
hasattr(config, "proxy_host") or "proxies" in http_config_kwargs
643+
)
644+
if has_param_proxies:
645+
return ProxySessionManager(config, **http_config_kwargs)
646+
else:
647+
return SessionManager(config, **http_config_kwargs)

src/snowflake/connector/ssl_wrap_socket.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
from .crl import CertRevocationCheckMode, CRLConfig, CRLValidator
2626
from .errorcode import ER_OCSP_RESPONSE_CERT_STATUS_REVOKED
2727
from .errors import OperationalError
28-
from .session_manager import SessionManager
28+
from .session_manager import SessionManager, SessionManagerFactory
2929
from .vendored.urllib3 import connection as connection_
3030
from .vendored.urllib3.contrib.pyopenssl import PyOpenSSLContext, WrappedSocket
3131
from .vendored.urllib3.util import ssl_ as ssl_
@@ -115,11 +115,15 @@ def get_current_session_manager(
115115
"""
116116
sm_weak_ref = _CURRENT_SESSION_MANAGER.get()
117117
if sm_weak_ref is None:
118-
return SessionManager() if create_default_if_missing else None
118+
return (
119+
SessionManagerFactory.get_manager() if create_default_if_missing else None
120+
)
119121
context_session_manager = sm_weak_ref()
120122

121123
if context_session_manager is None:
122-
return SessionManager() if create_default_if_missing else None
124+
return (
125+
SessionManagerFactory.get_manager() if create_default_if_missing else None
126+
)
123127

124128
return context_session_manager.clone(**clone_kwargs)
125129

0 commit comments

Comments
 (0)