Changelog
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Unreleased
1.0.1 - 2026-07-11
Changed
proxylib[quickjs]now installsquickjs-ng(an actively maintained, drop-in-compatible fork — sameimport quickjs/API) on Python 3.10+, which ships wheels through 3.13/3.14; Python 3.9 keeps the originalquickjspackage, which still has a cp39 wheel.quickjsis consequently now part of the CI/release install lines.- The default JS engine priority (
PROXYLIB_JS_ENGINEunset) now prefersquickjsoverdukpywhen both are installed, instead of the other way around —quickjsis the closer-to-spec/faster engine when its dependency happens to be present.dukpyremains whatproxylib[jspac]installs and the runtime fallback when quickjs isn't available.
1.0.0 - 2026-07-11
Added
ProxyDict(inproxylib.proxy): the plain proxies-dict integration, renamed and moved out of therequestsmodule since it never actually depended on requests — usable with anything that accepts a{scheme: proxy_uri}mapping.first_working_proxy()(inproxylib.netutils): failover helper that probes aProxyMapresult (PROXY a; PROXY b; DIRECT) in order and returns the first entry that accepts a TCP connection.- WPAD discovery results (including failures) are now cached per fqdn for 5
minutes (
discover(..., cache_ttl=...)to tune or disable) — previously, "auto-detect on but no WPAD server" re-probed DNS/HTTP with multi-second timeouts on every call. proxylib.__version__, resolved lazily from the installed package metadata.Documentationproject URL (the GitHub Pages docs site) in the package metadata shown on PyPI.- PEP 639
license = "MIT"/license-filesmetadata (replaces the license classifier). .github/workflows/test.yaml: on-demand test matrix (workflow_dispatch or a throwawayci-*tag push), separate from the release-gate test job — lets platform-specific code (GNOME/gsettings, macOS) be validated on real CI.NO_PROXYentries now accept CIDR notation (10.0.0.0/8,2001:db8::/32), matched against IP-literal request hosts.proxylib.set_default_no_proxy()/get_default_no_proxy(): process-wide defaultNO_PROXYrules, merged into everyEnvProxyConfig.WinHttpProxyMap(proxylib.os.nt): aProxyMapbacked by WinHTTP's ownWinHttpGetProxyForUrlautoproxy engine — WPAD/DHCP-252 discovery, NTLM/ Kerberos SSO for fetching the PAC, and PAC JS execution all happen in the OS, so nodukpyis needed on Windows.CFNetworkProxyMap(proxylib.os.darwin): the macOS equivalent, backed byCFNetworkCopyProxiesForURL/CFNetworkExecuteProxyAutoConfigurationURL.provider="system"param onauto_proxy()/system_proxy(): opts into the two native resolvers above (orLibProxyMapon POSIX, if its shared library is loadable) instead of proxylib's own Python-side detection/PAC path, which remains the default (provider="python").- gsettings-based detection (GNOME/MATE) now reads via one
gsettings list-recursivelycall instead of up to 7 single-keygsettings getcalls;shutil.whichlookups forgsettings/nmcli/dbus-sendare now cached (clearable, for tests). pac.load()now caches genuine network downloads (notfile:/inline-JS sources) for 5 minutes by default (cache_ttl=param).PAC.dnsResolveresults are now cached for 30 seconds by default (cache_ttl=param) — real-world PAC scripts often call it repeatedly for the same host.ChainProxyMap(*maps)(inproxylib.proxy): sequential fallback acrossProxyMaps — aKeyError(no opinion) tries the next one, a definitive[None]/[Proxy]result stops the chain.ConfigurableProxyMap(proxymap, ...)(inproxylib.proxy): decorates anyProxyMapwith opt-in caching (cache_ttl=), active reachability probing (probe=/probe_timeout=, vianetutils.first_working_proxy), round-robin selection (round_robin=), browser-style HTTPS privacy stripping (browser_compatibility=), and an implicit local-address bypass (bypass_local=) plusno_proxy=rules (merged with the globalset_default_no_proxy()defaults).netutils.is_loopback_or_link_local(): shared helper (loopback +169.254/16/fe80::/10) used by bothEnvProxyConfig's<local>handling andConfigurableProxyMap'sbypass_local=.- Extensible patching registry (
proxylib.patching):patch(proxymap, targets=None)/unpatch()/register_patcher(name, patch_func, unpatch_func)globally wire an activeProxyMapinto third-party HTTP clients. Built-in targets:"requests"(newly constructedSessions mount aProxyMapAdapter) and"urllib"(installs a global opener).ProxyMapinstances are now usable as a context manager (with proxymap as active: ...) equivalent topatch()/unpatch(). - Pluggable PAC JS engines (
proxylib.pac.engines):JSProxyAutoConfig/JSContextnow run against a smallJSEngineinterface instead ofdukpydirectly. New optionalquickjsbackend (proxylib[quickjs]extra) alongside the existingdukpyone (proxylib[dukpy]). Select with thePROXYLIB_JS_ENGINEenv var (comma-separated priority list, e.g.PROXYLIB_JS_ENGINE=quickjs,dukpy); defaults to preferringdukpy.proxylib[jspac]is now a meta-extra for "any working engine" (currently aliasesproxylib[dukpy]). - WPAD discovery now fails fast on a hanging DNS lookup:
socket.gethostbynamehas no timeout parameter, so it's now run in a worker thread with afuture.result(timeout=...)deadline (sametimeoutthe HTTP fetch already used) instead of blocking indefinitely. first_working_proxy()now has a circuit breaker: a proxy that fails its probe is skipped (without re-probing) for 30 seconds by default (circuit_breaker_ttl=), shared withConfigurableProxyMap(probe=True).JSProxyAutoConfig/JSContextaccept anoverrides=dict (JSProxyAutoConfig(js, overrides={"dnsResolve": fn, ...})) to replace a PAC utility function's Python-fallback implementation per-instance — simplifies offline/sandboxed testing and DNS mocking. A PAC script that redefines the same name in JS still overrides it, same as any other exported function.release.yamlnow scrapes the pushed tag'sCHANGELOG.mdsection into the GitHub Release body (this repo pushes commits directly, so GitHub's own auto-generated release notes are thin on every release).
Changed
pac.load()/WPAD fetches now go through an explicit no-proxy opener instead of the default (env-var-honoring) one — fetching a PAC/WPAD script must never itself be routed through a configured proxy.PAClookups now pass the full request URL (path and query included) toFindProxyForURL, per the PAC spec, instead of truncating toscheme://netloc— PAC scripts with path-based rules now work.EnvProxyConfigno longer resolves DNS on every lookup; resolution only happens when aNO_PROXY<local>entry actually needs it.- libproxy's shared library is now located lazily on first use instead of at
import proxylibtime. JSContext-exported callables (JSProxyAutoConfig's Python-backed PAC utility functions) are now pre-bound once at construction instead of a fresh closure allocated on every attribute access.proxylib.requests/proxylib.urllibmoved toproxylib.integrations.requests/proxylib.integrations.urllib— no shim kept at the old module paths (pre-1.0 breaking change; top-levelfrom proxylib import ProxyMapAdapter, ProxyMapHandleris unaffected).ProxyDictmoved fromproxylib.proxytoproxylib.integrations.dict, and no longer subclassesProxyMap(composition instead — its__getitem__returnsstr, which was an LSP violation as aProxyMapsubclass). Top-levelfrom proxylib import ProxyDictis unaffected.EnvProxyConfignow raisesKeyError(instead of returning DIRECT) for a scheme with no configured env proxy, so a futureChainProxyMapcan fall through to the next map instead of env "winning" by default.get_local_interfaces()results are now cached for 10s (cache_ttl=param) — interface enumeration blocks, and<local>-in-NO_PROXYlookups call it per request.<local>(NO_PROXY) now also bypasses link-local addresses (169.254/16,fe80::/10) unconditionally, not just when they happen to match a local interface's subnet.
Removed
RequestsProxies— renamed toProxyDict(see Added); the old name is gone. Only ever published under a release candidate (1.0.0-rc.1), so no stable release carried it.proxylib[requests]extra.requestssupport has always worked whenever the package happens to be installed (guarded bytry/except ImportError); installingrequestsisn't a proxylib feature, so there was nothing for this extra to add overpip install requestsitself.
Fixed
ProxyMapAdapterno longer loses to environment-injected proxies:requests.Session.merge_environment_settingsmergesHTTP_PROXY/HTTPS_PROXYinto theproxiesdict at the scheme level beforeadapter.send()runs, so the oldproxies.setdefault(resolved.scheme, ...)always lost whentrust_env=True(therequestsdefault), and a DIRECT result didn't disable the env proxy at all. Now writes the more-specificscheme://hostnamekeyselect_proxy()checks first. As a consequence, only an explicitproxies=entry at that same key still overrides theProxyMap's decision — a less-specific one (e.g. a bare"http"key) no longer does.- Proxy URLs with a trailing slash (
HTTP_PROXY=http://proxy:8080/, a very common shape) were misclassified as PAC-file URLs and routed to the PAC loader, which tried to fetch the proxy itself as a PAC script and failed. Both theProxyMap(...)factory andEnvProxyConfigare fixed (EnvProxyConfigadditionally never treats proxy values as PAC sources —PROXY_PAC/OS PAC settings cover that separately). URL.from_str("example.com")(bare hostname) now raises a clearValueErrorinstead of crashing withAttributeErrorlater.JSContextMetacompared the first character of attribute names against_JSCONTEXT_EXCLUDEinstead of the name itself (dormant — the exclude list was unused).PAC.dnsDomainLevelscounted split-length instead of dots (sub.example.comwrongly returned 3 instead of 2, per the PAC spec's own definition).PAC.localHostOrDomainIsused a string-prefix check (hostdom.startswith(host)), so e.g."ww"wrongly matched"www.example.com"; now compares the exact host part.
1.0.0-rc.1 - 2026-07-09
First tracked release. proxylib graduates from a small proxy-string-parsing
utility into a platform-agnostic "what proxy should I use for this request"
library, with real system detection on every major OS/desktop, WPAD
discovery, full PAC compliance, and integrations for both requests and
urllib.
Added
- Cross-platform system proxy auto-detection (
system_proxy()/auto_proxy()): - Windows, via the WinHTTP API (
WinHttpGetIEProxyConfigForCurrentUser) — respects "Automatically detect settings", the configured PAC script, and a manual proxy, in that precedence order. - macOS, via
scutil --proxy— respects "Auto Proxy Discovery" (WPAD), "Automatic Proxy Configuration" (PAC), and a manual proxy. - Linux, via (in order) libproxy
(through direct
ctypesbindings to its C API), GNOME/MATE (gsettings), KDE (kioslaverc), and NetworkManager (nmcli, ordbus-senddirectly ifnmcliisn't installed) — plus the existingHTTP_PROXY/HTTPS_PROXY/NO_PROXY/PROXY_PACenv var fallback. LibProxyMap, libproxy's own resolution, also available as a standaloneProxyMapusable directly on any platform.- WPAD (Web Proxy Auto-Discovery) support via DNS + HTTP
(
wpad.<domain>/wpad.dat), used both by each platform's own auto-detect signal above and as a genericauto_proxy()fallback when nothing else is configured. DHCP-based discovery (option 252) is out of scope — no portable stdlib path to OS DHCP lease data. ProxyMapAdapter, arequestsTransport Adapter that resolves the proxy from the real request URL (scheme, host, and path), not just scheme+host.ProxyMapHandler, the equivalent integration forurllib.request.- Full Netscape PAC utility-function support:
dateRange/timeRangeare real implementations now (previously always returnedFalse), andisInNetis IPv6-tolerant. Added the common Microsoft*Exextensions real-world WPAD files rely on:dnsResolveEx,myIpAddressEx,isResolvableEx,isInNetEx,sortIpAddressList,getClientVersion. - PEP 561 typing support (
py.typed) and a full type-hint pass. - Substantially expanded test suite (5 tests → 110+) and documentation
(rewritten
README.md, docstrings throughout).
Changed
requires-pythonlowered from>=3.11to>=3.9.os/posix.pyrestructured into anos/posix/package, one module per backend, now that there are five of them.- New optional extra:
proxylib[requests], forProxyMapAdapter.
Fixed
Several of these change observable behavior for anyone relying on the old (broken) results:
SimpleProxyMap(str)always parsed with the PAC grammar (PROXY host:port; DIRECT), which has no concept of"://"— soSimpleProxyMap("http://proxy:8080")(the shapeHTTP_PROXY/HTTPS_PROXYenv vars actually use) silently produced a proxy with an empty host. This brokeEnvProxyConfigfor the common case.SimpleProxyMaptreated a singleProxyas a sequence of its own tuple fields instead of wrapping it in a 1-tuple, sinceProxy(aNamedTuple) is itself aSequence._URI.resolved()computed a corrected copy but never returned it.EnvProxyConfig'sNO_PROXYmatching was anchored at the wrong position — a plain host entry likeexample.comcould never matchhttps://example.comat all. Separately,from_env()passed the raw, unsplitNO_PROXYstring into a constructor expecting a list of entries. Also,<local>handling calledis_loopback()as a method instead of a property, raisingTypeErrorwhenever it was actually exercised.PAC.weekdayRangeindexed by weekday abbreviation instead of index.netutils.get_default_portrelied solely onsocket.getservbyname, which doesn't reliably knowhttps/socks4/socks5across platforms.- The
pac.load()warning referenced installingproxylib[pac]; the real extra name isproxylib[jspac].