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
2.0.2 - 2026-10-20
Fixed
- pip failed in an environment with proxylib 2.0.1 installed whenever it used its
HTTP cache (the default, and the build-dependency step of every source build): pip's
caching adapter passes
send()arguments by position, which the pip target's hook did not take (TypeError: send() takes 2 positional arguments but 7 were given). The hook andProxyMapAdapter.sendnow take them. To upgrade from 2.0.1, turn the hook off for that one command:IMPORTPATCH_DISABLE=1 pip install -U proxylib(PowerShell:$env:IMPORTPATCH_DISABLE=1; pip install -U proxylib). - requests (and pip): a caller's own proxies beat the patched map again, as requests
itself does —
proxies={"http": ...}or{"all": ...}on the call, orsession.proxies; before, only ascheme://hostentry did, since the adapter could not tell a caller's entry from one requests adds from the environment. The requests patch target now records the caller's proxies before requests merges the environment's. A proxylib value given by the caller (a PAC result, a gateway) is still read. - A session with
trust_env=Falseis left to requests again: the map of the adapters the patch target mounts (the OS's or the environment's proxy, or apatch()) is not consulted, as pip's--proxyalready was. An adapter the program mounts itself keeps its map.
2.0.1 - 2026-10-20
Added
PROXYLIB_AUTOPROXY(1, or client names separated by:) makes those clients useauto_proxy()— the proxy variables, the OS settings, PAC and WPAD. This is whatPROXYLIB_IMPORTPATCH=1or a name did in 2.0.0.- The
pippatch target: pip's vendored requests (pip._vendor.requests) routes every request through the active map likeProxyMapAdapter— failover, gateways, proxy headers — through an importpatch entry point.pip --proxyand--no-proxy-envkeep pip's own choice, and pip's TLS context is left as it is.
Fixed
- The default integration now does what 2.0.0 meant it to: every client —
requests,urllib,httpx,aiohttpand pip — keeps its own proxy choice and understands proxylib's formats where it takes a proxy.HTTP_PROXY="PROXY a:3128; PROXY b:3128; DIRECT"(which urllib, httpx and aiohttp rejected as an invalid URL) fails over along the list, and a gateway (prefix+https://gw/fetch/) is used;NO_PROXYstill applies. PROXYLIB_IMPORTPATCHchooses which clients get the integration (unset: every one;0: none untilpatch()), not which useauto_proxy(); that isPROXYLIB_AUTOPROXY.- With the aiohttp target installed, a DIRECT answer is direct even in a session that trusts the environment.
2.0.0 - 2026-10-19
2.0 breaks the 1.x API. The migration guide maps every removed or renamed 1.x name to its 2.0 form.
Added
proxylib.exceptions:ProxylibErrorand its subclassesProxyParseError(also aValueError),PACError,NoReachableProxyError(also aLookupError) andMissingExtraError(also anImportError), re-exported at top level. None of them is aKeyError, which keeps meaning "no opinion".proxylib.clear_caches()drops every module-level cache in one call.proxylib.__all__declares the public surface.- PAC is split into
pac.functions(the builtins),PacScript(one script in a JS engine) andPacProxyMap(the map, which also takes a PythonFindProxyForURLfunction).pac.loads(text)/proxylib.loads_pacload a script from text;pac.engines.names()lists the installed engines. - PAC scripts run in a helper process with a time limit (
script_timeout=, default 5 s per call): a call that hangs raisesPACErrorinstead of hanging the program, and the script fails fast for 30 s before it is restarted. Neither engine can be stopped in-process. The DNS builtins (dnsResolve,dnsResolveEx,isResolvable,myIpAddress) are bounded at 2 s each. lookup(proxymap, url)reads any proxy map's answer as a tuple ofProxyorNone(DIRECT). It accepts every answer shape:None, aProxy, a proxy URL, a PAC result string, any object with aurlattribute, or an iterable of those; an empty answer is DIRECT. A plain mapping is also asked with the keysrequestsuses (scheme://host,scheme,all://host,all).Proxy.parse(text)andProxy.parse_pac_result(text), and theProxyLike/ProxyAnswertype aliases.BypassRules: one bypass-list type with parsers forNO_PROXY(curl), WindowsProxyOverride, macOSExceptionsList, GNOMEignore-hostsand KDENoProxyFor(includingReversedException); IPv6 CIDR works. The OS backends now read their own bypass lists with their own syntax (a WinINet*.corp;10.*list, a macOS*.local, a KDE reversed list).system_config()returns aProxyConfig: the OS or desktop settings as data (source,state="direct"/"configured"/"unset",auto_detect,pac_url,pac_script, per-schemeproxiesplus a"socks"fallback, andbypass).ProxyConfig.to_proxymap()builds the map: WPAD, then the PAC, then the proxies, each passing to the next when it fails.- Wrappers in
proxylib.wrappers, composed in the order written:Bypass(inner, rules),Cached(inner, ttl=, maxsize=1024),RoundRobin(inner)andProbed(inner, timeout=5.0), plusconfigure()for the common combination.ProbedraisesNoReachableProxyErrorwhen nothing is reachable (it wasKeyError). patch()returns aPatchhandle: patches nest (with patch(a): with patch(b):restoresaafter the inner block),undo()ends one, andunpatch()ends all.patching.active(target)is the map a client uses now;patching.targets()lists the clients.- Client patching:
requests(new sessions) andurllib(its default opener, when the program has not installed its own) get proxylib's adapter and handler as they are imported, in any program of the environment, through proxylib's importpatch entry points (importpatch is a dependency;IMPORTPATCH_DISABLE=1orpyimportpatch.cfgturn that part off). Untilpatch()is called they choose the proxy exactly as the client would on its own. PROXYLIB_IMPORTPATCH:1for every client, or client names separated by:(requests:httpx), makes those clients useauto_proxy();0installs nothing untilpatch().httpx(Client/AsyncClientbuilt without a transport, proxy or mounts) andaiohttp(ClientSessionrequests withoutproxy=) are patch targets too, installed only when named or patched from code.close()on every built-in map: it frees what the map owns (a PAC helper process, an OS session) and closes the maps it wraps or chains.- Failover:
ProxyMapAdapter(proxymap, failover=True, bad_proxy_ttl=300.0)tries an answer's entries in order, healthy ones first; an entry that fails before the request is sent (refused or timed-out connection) is skipped forbad_proxy_ttlseconds and the next is tried. A read or write error is never retried.ProxyMapHandleruses the healthiest entry. The bad-proxy marks are shared withProbed. - A proxylib value in requests'
proxies— a PAC result such as"PROXY a:1; DIRECT", a list of proxies, or aProxyDict— is resolved and failed over by the adapter (which importing proxylib installs); a plain proxy URL stays requests' own. proxylib.integrations.httpx:ProxyTransportandAsyncProxyTransportforhttpx.Client(transport=...)/httpx.AsyncClient(transport=...), with the same failover (httpx itself is not a dependency).alookup(proxymap, url):lookup()for asyncio code, run in a worker thread.proxylib.integrations.aiohttp.ProxySession: wraps anaiohttp.ClientSession, taking each request'sproxy=from a proxy map, with the same failover.- The system backends log their decisions at DEBUG on the
proxylib.systemlogger. - Proxy headers:
Proxy(..., headers={"X-Proxy-Token": "..."})(or aProxy-Authorizationvalue such as a Bearer token) is sent to that proxy only — in theCONNECTof a tunnel and on a plain-http request to it, never to the server.repr()hides the values. - URL-prefix gateways:
prefix+http(s)://host[:port]/path/(Proxy(..., path=...)), orPREFIX https://gw/path/in a PAC result, sends the request for a URL directly to<gateway><URL>; requests, httpx and urllib keep the target as the response's URL, so cookies and redirects belong to it. UnsupportedProxyError: each client integration skips the entries its client cannot use (gateways and SOCKS for aiohttp, SOCKS for urllib; for urllib, headers other thanProxy-Authorizationon https URLs; gateways and headers forProxyDict) and raises this when nothing usable is left.- A proxy variable may hold a whole PAC result:
HTTP_PROXY="PROXY a:3128; SOCKS5 b:1080; DIRECT"is tried in that order.
Changed
ProxyMapis a structural protocol with only__getitem__: a plaindict, or any object indexable by URL, is a proxy map.get(),inand the context manager (usepatch()/unpatch()) are gone.ProxyMap(src)still builds a map from a configured value, now without any I/O: a PAC location is loaded on its first lookup, and an existing map is returned unchanged.Proxyis an immutable value class,Proxy(scheme, host, port=None, username=None, password=None); absent port and credentials areNone(1.x:0and"").as_uri()is nowuri()and percent-encodes the credentials;Proxy("direct", ...)no longer returnsNone.- Built-in maps return tuples. A malformed proxy or request URL raises
ProxyParseError(aValueError). Bracketed IPv6 hosts parse (http://[::1]:8080), and a scheme-lesshost:portis anhttpproxy. EnvProxyConfig(env=None, *, proxies=None, no_proxy=None)replaces the three positional arguments andfrom_env(): it readsenv(defaultos.environ, any mapping accepted), withproxies/no_proxyapplied over it. Every<scheme>_proxyis read, plusall_proxy;ws/wssuse thehttp/httpsproxies; a lowercase name now wins over uppercase (stdlib and curl order); a PAC URL may be given as a proxy value.<local>in a bypass list now means a host name without a dot (its WinINet meaning); the 1.x meaning (loopback, link-local or same subnet) is<subnet>.- Loopback (
localhost,127.0.0.0/8,::1) bypasses the proxy wherever bypass rules apply, unless<-loopback>is listed. InNO_PROXY,*among other entries is ignored with a warning, as curl and requests do. pac.load(location)takes a URL or a path only; script text goes topac.loads(). Download options are named (timeout=,cache_ttl=); the**urllib_kwdspass-through is gone. A body over 1 MiB, a script error, a script withoutFindProxyForURLand a time-limit overrun raisePACError.- With no JS engine installed, loading a PAC raises
MissingExtraError;on_missing_engine="direct"restores the old warn-and-DIRECT behaviour. PROXYLIB_JS_ENGINEorengine=naming an unknown engine raisesValueError(it was ignored). Engines are imported only when a script needs one.wpad.discover()returns aPacProxyMap, takes keyword-only options and skips a domain whose PAC fails to run.- proxylib now depends on
netimps>=0.4.0,<0.5for host:port handling, local interfaces (real prefix lengths on every OS, no extra needed), bounded name lookups and TCP probes, and onimportpatch>=0.2.1,<0.3for post-import hooks.import proxylibimports importpatch, but neither netimps norrequests. - Extras:
proxylib[pac]installs the quickjs engine andproxylib[dukpy]the dukpy one;jspac,quickjsandifaddrare gone. ProxyMapAdapteris always a class: constructing it withoutrequestsraisesMissingExtraErrorinstead of the name beingNone.patch(proxymap, *, targets=None)no longer replaces the active patch with a warning; patches stack. A patch now also reaches sessions created before it (the adapter asks the active map on every request). With notargetsa missing client is skipped; naming it raisesMissingExtraError.register_patcher(name, install, *, uninstall, module, unnamed=False, replace=False):installruns once whenmoduleis imported (and the target is wanted) and receives a map that follows the active patch; registering an existing name raisesValueErrorunlessreplace=True.ProxyMapAdapter: aProxyDictgiven asproxieswins over the adapter's map, as a caller'sscheme://hostnameentry does; such an entry is sent as given, without failing over along the map's answer.ProxyMapHandler: when the map has no opinion, the request usesurllib.request.getproxies()for its scheme, as the standard handler does (it went DIRECT).ProxyMapAdapterpasses aproxiesmapping of any type through (aProxyDictincluded) instead of copying it into adict.- WPAD and PAC
dnsResolvelookups are bounded by a timeout. weekdayRange,dateRangeandtimeRangeanswer as Chromium's PAC library (shared with Firefox):dateRangehandles every combination of days, months and years (two-value forms such asdateRange(24, "DEC")were always false),timeRange(h1, h2)no longer wraps past midnight (timeRange(22, 0, 6, 0)does), and an unknown day or month name is false instead of an error.PacProxyMapover a Python function: whatever the function raises becomesPACError(aKeyErrorfrom it was read as "no opinion").auto_proxy(*, system=True, no_proxy=None, wpad=False, timeout=10.0, on_missing_engine="raise", environ=None). The proxy environment variables come first on every OS: when any is set they alone decide, and the OS is not consulted (Windows and macOS ignored them before; Linux desktops outranked them). Otherwise the OS's own resolver answers by default (WinHTTP, CFNetwork, libproxy when installed), backed by the OS settings for a URL it cannot answer;system=Falseuses only the settings, and libproxy is never used there.- WPAD runs only when the OS has auto-detection switched on, or with
wpad=Trueon a machine with no settings. An OS that says "no proxy" is final; 1.x ran WPAD whenever nothing was configured. - SOCKS-only and per-protocol OS settings are kept: a Windows
socks=entry (SOCKS v4) or a macOS/GNOME/KDE SOCKS proxy (v5) serves every scheme without its own proxy, and anhttps-only proxy no longer serveshttpURLs (or the reverse). FTP proxies are read too. - On Linux, the first desktop with a proxy configured wins, so another desktop's
mode='none'default no longer hides KDE's settings. proxylib.osis nowproxylib.system, with private per-OS modules;WinHttpProxyMapisWinHTTPProxyMap. The WinHTTP bindings load on first use, so the module imports on every OS.- External programs (
scutil,gsettings,nmcli,dbus-send) run through one helper: no stdin,LC_ALL=C, UTF-8 output, 5 s limit.
Security
- A PAC script runs in a separate helper process with a time limit per call (and a memory limit with quickjs): a hostile or broken script can no longer hang or exhaust the program.
- WPAD runs only when the OS has auto-detection switched on (or
wpad=Trueis passed), so a network that answers forwpad.<domain>cannot choose the proxy of a machine that never asked for auto-detection.
Removed
PAC,JSProxyAutoConfig,pac.javascript.JSContext,pac.clear_download_cache(),pac.clear_dns_cache()andpac.engines.get_engine_class(): usePacProxyMapwith aPacScriptor a Python function,proxylib.clear_caches()andpac.engines.get_engine().set_default_no_proxy()/get_default_no_proxy(): pass bypass rules where a map is built (auto_proxy(no_proxy=...),EnvProxyConfig(no_proxy=...),Bypass(proxymap, rules)).UriSplit,Proxy.from_str,Proxy.find_allandProxy.as_uri: useProxy.parse,Proxy.parse_pac_resultandProxy.uri.proxylib.netutilsand the top-levelfirst_working_proxy; useProbedfor reachability probing.ConfigurableProxyMap: use the wrappers (Cached,RoundRobin,Probed,Bypass) orconfigure();browser_compatibilityis onPacProxyMap, andbypass_local=Trueis the<subnet>bypass token.system_proxy()andauto_proxy(provider=...): usesystem_config()andauto_proxy(system=...). ThePROXY_PACvariable: give the PAC location inHTTP_PROXYand the other proxy variables.LibProxyMapis no longer exported at top level (proxylib.system.LibProxyMap), and the per-desktopdetect()functions are gone.
1.1.0 - 2026-10-18
Added
ConfigurableProxyMap(cache_maxsize=1024)bounds the per-URL cache (least recently used dropped first).LibProxyMap.close()frees the libproxy factory the map now keeps.PACandJSProxyAutoConfigareProxyMaps, so they gain the protocol'swithsupport (see Fixed).
Security
EnvProxyConfig.from_env()ignoresHTTP_PROXYinside a CGI request (REQUEST_METHODset), where a client'sProxy:header can set it (httpoxy, CVE-2016-5385). Only an exact lowercasehttp_proxyis used there, as inurllib.request.getproxies_environment; on Windows, where environment names are case-insensitive, neither is.- WPAD discovery no longer walks up to a public suffix: for
host.example.co.ukit trieswpad.example.co.ukonly, neverwpad.co.uk, which anyone can register. Proxy'srepr()andstr()showpassword='***'instead of the password.as_uri()still includes it.
Fixed
- One PAC map shared between threads no longer crashes the interpreter. Both JS engines abort when two threads enter one context; calls into the engine are now serialised per PAC instance.
- macOS
system_proxy()no longer raises on the default bypass list: abbreviated IPv4 CIDR such as169.254/16is accepted in everyNO_PROXYlist, and an entry that still cannot be parsed is skipped with a warning instead of failing the whole configuration. pac.load()downloads time out after 10 seconds unlesstimeout=is passed, refuse a body over 1 MiB (Chromium's limit) withValueError, and decode a script that is not UTF-8 as Latin-1 instead of raising.PACandJSProxyAutoConfigareProxyMaps, as the docs always said:isinstance(pac, ProxyMap)is true andwith load_pac(...):patches like any other map, sowith auto_proxy():no longer fails when a PAC is in effect.JSProxyAutoConfig.get()no longer raises: it was being routed into the JS engine.- A PAC result entry with an unknown keyword or no host (
"garbage","PROXY ") is skipped, and a result with nothing usable left — including an empty string or a non-string return — means DIRECT, as in browsers. It previously produced host-lessProxyobjects or an empty list. PAC.shExpMatchtreats only*and?as wildcards and matches case-sensitively on every platform, as Mozilla and Chromium do. It usedfnmatch, which is case-insensitive on Windows and reads[...]as a character class.ProxyDicttreats a key its map cannot parse as a URL ("http","all", whichrequestslooks up first) as missing.requests.get(..., proxies=ProxyDict(EnvProxyConfig.from_env()))raisedValueError.ConfigurableProxyMap(cache_ttl=..., round_robin=True)rotates on every lookup; the cache had frozen the first rotation. The cache is bounded by the newcache_maxsize=1024keyword (least recently used dropped first).ConfigurableProxyMapapplies theset_default_no_proxy()rules even when built withoutno_proxy=orbypass_local=.- Windows: a WinHTTP answer listing several proxies (
p1:8080;p2:8080) keeps every applicable entry in order; only the first was used.WinHttpProxyMapsets 10-second session timeouts, so a PAC fetch cannot hang on name resolution. - macOS: an HTTPS proxy from
CFNetworkProxyMapis returned as anhttp://proxy. It is a plain proxy reached withCONNECT, and clients were trying to speak TLS to it. An entry of an unknown type is skipped instead of meaning DIRECT. - Linux: GNOME/MATE settings are re-read after 10 seconds instead of being
cached for the life of the process; a
kioslavercthat is not UTF-8 counts as not configured instead of raising; thenmclipath checks every active connection, as the D-Bus path does. LibProxyMapkeeps one libproxy factory per map instead of building one per lookup (which re-fetched the PAC every time), frees it in the newclose(), and treats a failed libproxy call as no opinion (KeyError) instead of DIRECT.system_proxy()andauto_proxy()raiseValueErrorfor an unknownprovider; a typo used to fall back to"python"silently.- An IPv6 proxy host is bracketed in
Proxy.urlandas_uri()(http://[::1]:8080). pac.load()no longer caches afile://URL, as documented for file sources.- Public annotations resolve with
typing.get_type_hintson Python 3.9; they were written as"X|None"strings, which 3.9 cannot evaluate.
1.0.2 - 2026-08-16
Added
NO_PROXY="*"now bypasses the proxy for every host, matching curl,requests.utils.should_bypass_proxiesand the stdlib'surllib.request.proxy_bypass_environment. It previously parsed to an ordinary host entry that could never match a real hostname, so the common "stop proxying everything right now" escape hatch silently did nothing. Works viaNO_PROXY/no_proxy, an explicitEnvProxyConfig(..., no_proxy=["*"]),ConfigurableProxyMap(no_proxy=["*"]), andset_default_no_proxy(["*"]); it is an explicit DIRECT result, so it applies even when no proxy is configured for the scheme, and it is checked ahead of every other rule so it never triggers a<local>DNS lookup.
Fixed
- URI scheme parsing no longer accepts a comma. The scheme character class was
written
[A-Za-z0-9+-.], in which+-.is the range0x2B–0x2Eand so also matched,(0x2C) —http,x://host:1parsed as a scheme ofhttp,x. RFC 3986 allowsALPHA/DIGIT/+/-/.only. - The GNOME/MATE gsettings backend no longer builds
http://host:Nonewhen the schema has a proxy host but no usable port (key absent, or0for "never configured"); the port suffix is omitted so the scheme's default applies. - Documentation drift that shipped in the wheel and rendered on the docs site:
the
proxylibAPI header no longer points at a repo-rootAGENTS.mdthat doesn't exist (it links the project README instead), therequestsintegration no longer tells users to install aproxylib[requests]extra that was removed before 1.0 (requestssupport activates wheneverrequestsis installed), the POSIX backend no longer claims libproxy is used via aproxyCLI (it binds the shared library throughctypes), and the PAC module/load()docstrings are engine-agnostic again —dukpystopped being the only option when the engine split landed.
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].