Switch proxy auth from URL-embedded to Proxy-Authorization header

Embedding credentials in the proxy URL (http://user:pass@host:port) was
breaking HTTPS CONNECT tunnels on the hotel network. Switching to separate
username/password fields (Playwright) and httpx.Proxy(auth=...) sends a
Proxy-Authorization header instead, which passes through correctly.
With DataImpulse IP whitelisting the 407 round-trip is skipped anyway so
there is no latency penalty.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-07-10 02:13:06 +00:00
parent 3b931d4915
commit 940ffbbe39
2 changed files with 33 additions and 21 deletions

View file

@ -78,33 +78,46 @@ def username(cfg: dict, session_id: Optional[str] = None) -> str:
def playwright_proxy(cfg: dict, session_id: Optional[str] = None) -> Optional[dict]:
"""Proxy dict for chromium.launch(proxy=...). None when disabled.
"""Proxy dict for new_context(proxy=...). None when disabled.
Credentials are embedded in the server URL rather than passed as separate
fields. Separate fields cause Chromium to wait for a 407 challenge before
sending auth DataImpulse takes ~14s to issue that challenge, making every
page.goto() timeout. Embedded credentials are sent on the first CONNECT
request, bypassing the round-trip entirely.
Uses separate username/password fields so Chromium sends a
Proxy-Authorization header rather than embedding credentials in the URL.
With IP whitelisting on DataImpulse, the 407 round-trip is skipped entirely
(the proxy accepts on IP alone), so there is no speed penalty.
"""
if not is_enabled(cfg):
return None
from urllib.parse import quote
user = quote(username(cfg, session_id), safe='')
pwd = quote(cfg['password'], safe='')
return {
'server': f"http://{user}:{pwd}@{cfg['host']}:{cfg['port']}",
'server': f"http://{cfg['host']}:{cfg['port']}",
'username': username(cfg, session_id),
'password': cfg.get('password', ''),
}
def httpx_proxy_url(cfg: dict, session_id: Optional[str] = None) -> Optional[str]:
"""Proxy URL for httpx.AsyncClient(proxies=...). None when disabled."""
def httpx_proxy(cfg: dict, session_id: Optional[str] = None):
"""httpx.Proxy object for AsyncClient(proxy=...). None when disabled.
Uses the auth= kwarg so credentials are sent as a Proxy-Authorization
header rather than embedded in the URL.
"""
if not is_enabled(cfg):
return None
return f"http://{username(cfg, session_id)}:{cfg['password']}@{cfg['host']}:{cfg['port']}"
import httpx
return httpx.Proxy(
f"http://{cfg['host']}:{cfg['port']}",
auth=(username(cfg, session_id), cfg.get('password', '')),
)
def direct_httpx_proxy(db) -> Optional[str]:
"""Proxy URL for the direct booking-engine scraper — only when the proxy is
def httpx_proxy_url(cfg: dict, session_id: Optional[str] = None) -> Optional[str]:
"""Legacy URL form — prefer httpx_proxy() for new callers."""
if not is_enabled(cfg):
return None
return f"http://{cfg['host']}:{cfg['port']}"
def direct_httpx_proxy(db):
"""httpx.Proxy for the direct booking-engine scraper — only when the proxy is
configured AND `direct_scraper_use_proxy` is explicitly enabled (default off).
Uses a fresh session id per call so direct runs spread across IPs."""
from sqlalchemy import text
@ -113,4 +126,4 @@ def direct_httpx_proxy(db) -> Optional[str]:
).fetchone()
if not flag or flag.config_value != 'true':
return None
return httpx_proxy_url(load_config(db), new_session_id())
return httpx_proxy(load_config(db), new_session_id())