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
0.1.0 - 2026-10-20
Added
syshttp.client: an HTTP client on the standard library, so that no feature of syshttp needsrequests(which onlysyshttp.autopatchpatches, in a program that imports it).HTTPClient(transport=None, *, headers, timeout, deadline, user_agent, env, proxymap, ssl_context, proxy_ssl_context, verify, cookies, tokens, auth_provider, retries, backoff, status_forcelist, cookie_hosts)sends through a transport and follows redirects with one rule for credentials (Authorization,CookieandProxy-Authorizationgo to the origin they were given for and to no other scheme, host or port). The defaultHTTPTransport(onhttp.client, with a per-thread connection pool, framed bodies,gzip/deflatedecoding and a bounded reply size) routes every request and redirect hop byproxy.proxy_map(env=env)(orproxymap=): the first entry that accepts a connection, anhttporhttpsproxy (CONNECT; anhttpsproxy over TLS with the origin's TLS inside it), a URL-prefix gateway, a proxy's credential to the proxy alone; and verifies with the caller'sssl_context, else theCA_BUNDLEbundle orverify=<path>, else the system's context (proxy_ssl_contextfor anhttpsproxy or gateway). Cookies (RFC 6265, per origin, on every hop, in a jar by host withcookies=), tokens,_PYhooks,auth_providerand retries (Retry-Afterhonoured, a status that outlasts them returned) work as they did on the session.HTTPResponsereads its body whole ascontent,text()andjson().syshttp.fetch()(defined insyshttp.client, re-exported from the root without importing it):fetch(url, method="GET", *, headers, data, json, timeout, client, transport, reauth, **client_options)returns theHTTPResponsewith its body read, whatever the status; a givenclientis left open and a new one is closed;reauthruns once after a 401 or 403.- The transport protocol is public:
Transport(send()andclose()),PreparedRequestandprepare_request()give another transport (onrequests,httpx, or a fake) the checks and body framing of syshttp's own.SYSHTTP_DEFAULT_TRANSPORT(module:attror<file>.py:attr, read under the prefixes) names the transport of a client given none; a value that cannot be used isConfigurationError.hooks.load_object()is the loader it shares with the_PYhooks. syshttp.exceptions:SysHTTPError(the base of what goes wrong at run time),ConfigurationError(aValueError),HookError(aRuntimeError) andResponseError(aValueError), exported from the root. A malformed setting, a hook that cannot be loaded, a failing hook and a body that is not JSON raise them; every one is still theValueErrororRuntimeErrorit was, so existing handlers keep working. A caller's own mistake stays a plainValueError/TypeError.- Every public module declares
__all__(proxyalso lists its setting-name constants,PROXYandDEFAULT_AUTH_HEADERamong them). syshttp.env.HTTPEnv(prefixes=None, environ=None): every setting is a variable with a full name -- syshttp's own carrySYSHTTP_(SYSHTTP_PROXY,SYSHTTP_HOME,SYSHTTP_HOOKS_<KEY>,SYSHTTP_IMPORTPATCH), conventional ones keep theirs (HTTPS_PROXY,NO_PROXY,HTTP_PROXY_AUTH,CA_BUNDLE) -- read as<PREFIX><NAME>for each prefix ofSYSHTTP_PREFIXin turn, a:-separated list used literally (default: one empty prefix, the names as written). The first prefix that sets a variable wins, each variable on its own:SYSHTTP_PREFIX=MYTOOL_reads onlyMYTOOL_<NAME>,MYTOOL_:falls back to the unprefixed names.find()returns the variable found with its value, for messages. Every function that reads a setting takesenv=,new_session()and thefetch*_autohelpers included.CA_BUNDLE(under the prefixes) names the CA bundle a session'sverify=Trueuses; sessions no longer read requests' ownREQUESTS_CA_BUNDLE/CURL_CA_BUNDLE.- The system's proxy configuration is the last proxy source, decided for the
URL being requested: WinHTTP on Windows, CFNetwork on macOS, libproxy or the
desktop's settings elsewhere, PAC scripts and WPAD included (through
proxylib, a new dependency). It comes after
SYSHTTP_PROXYand the*_PROXYvariables, after theNO_PROXYcheck, and is always spoken to asconnect.SYSHTTP_SYSTEM_PROXY=0turns it off.proxy.plan(),proxy_url(),resolve()andproxy_authorization()take the request URL (url=/target=).proxy.configured_proxy_url()gives the environment's answer alone. Thepacextra adds a JavaScript engine for PAC scripts proxylib evaluates itself (Linux without libproxy). syshttp.autopatch:SYSHTTP_IMPORTPATCH=1(or:-separated names:requests,urllib,httpx,aiohttp; read under the prefixes like every setting) patches those libraries as they are imported, in any program of the environment, so requests without a proxy or TLS setting of the caller's route by syshttp and verify with the system's context. Through importpatch, a new dependency, and its entry points;patch()/unpatch()do it from code.- The system's answer is a list (
PROXY a; PROXY b; DIRECT): the first proxy that accepts a connection is used (proxylib'sfirst_working_proxy), and a DIRECT reached first is direct. No reachable proxy and no DIRECT raisesproxy.NoReachableProxyError(aConnectionError, exported from the root); a session raisesrequests.exceptions.ProxyErrorfor it. new_session(ssl_context=..., proxy_ssl_context=...)(and the same on thefetch*_autohelpers): a caller'sssl.SSLContextis used as given for every https origin, or for anhttps://proxy. That covers mutual TLS, ciphers and protocol versions. It cannot be combined with averifyother thanTrue.hooks.wrapper_environment(): theSYSHTTP_HOOK_*variables a_CURLwrapper run receives.jar.default_root(): the per-user state directory used whenSYSHTTP_HOMEis unset.syshttp.__version__.- Hosts that share cookies:
SYSHTTP_COOKIE_HOSTS(hosts split by,, groups by;) ornew_session(cookie_hosts=...)declares names that reach one service. In a group, a cookie a member sets for itself or another member belongs to the first host, every member's requests carry the group's cookies, and any member can delete them; aDomainoutside the group is still judged on the host that sent it.cookies.host_aliases()builds the mapping, whichset_cookie_specs,merge_set_cookie_headersandhost_only_policytake asaliases=. - The shipped header and the settings page list every variable syshttp
reads under its own name (the proxy family,
REQUEST_METHOD, the CA bundle variables, the state-directory variables, importpatch's and proxylib's).
Changed
syshttp.fetchis a function, not a module:fetch()replaces the helpers below._PYhooks are called ashook(client, method, url, kwargs)with the keyword arguments ofHTTPClient.request(), and may return anHTTPResponse;auth.AuthProvider.ensure(client, url)takes the client.CA_BUNDLEis the bundle a client'sverify=Trueuses.cookies.pykeeps the Netscape rows,set_cookie_specs,cookie_appliesandhost_aliasesonly; the client keeps its cookies as those rows.- Routing is proxylib 2's (
proxylib>=2.0.2,<3; 2.0.1 breaks pip's cached requests).proxy.proxy_map(proxy, noproxy, proxy_user, *, env)is the settings as one proxylib proxy map, read at each lookup;proxy.configured_proxy()isSYSHTTP_PROXYas a proxylibProxy;proxy.system_map()is the system's configuration (proxylib.auto_proxy). The*_PROXYvariables are read per scheme (lowercase first, httpoxy-safe),NO_PROXYtakes CIDR entries, and curl's-x ""is direct.SYSHTTP_PROXY_AUTHgoes toSYSHTTP_PROXYonly andHTTP_PROXY_AUTHto the machine's proxies only. - A list from the system (
PROXY a; PROXY b; DIRECT) is tried in order by the session, httpx and aiohttp (proxylib's failover), replacing the probe of the entry above; urllib takes the first reachable entry. No proxy connecting and no DIRECT is the client's own connection error;NoReachableProxyErroris proxylib's. - The session's
ProxyAdapteris proxylib'sProxyMapAdapterwith the system's TLS;adapter.route(url)returns the entries it will try.proxies=given tonew_sessionare requests' own, with no syshttp credential added. -
syshttp.autopatchroutes throughproxylib.patch()and patches TLS only; requests' TLS is a pool hook on everyHTTPAdapter. A caller's own proxies andtrust_env = Falsestill win over syshttp (proxylib 2.0.2's rule). aiohttp is not offered URL-prefix gateways. New targetpip(pip._vendor.requests, proxylib 2.0.1'spiptarget): pip's adapters route through the map unless pip is given--proxyor--no-proxy-env; pip keeps its own TLS context. -
The jar constructors (
FileCookieJar,FileTokenJar) no longer create their directory; it is created at the first write. -
A session returns the last response when a status in
status_forcelistoutlasts its retries, as plain requests does; it used to raiserequests.exceptions.RetryError, which the fetch helpers (-> (status, text)) passed on. The*_autohelpers takenew_session's options as**session_options(every keyword they had still works) and theirtimeoutmay be a(connect, read)pair. -
Every dependency's floor is its newest release that supports Python 3.9: the
sessionextra requiresrequests>=2.32.5andurllib3>=2.6.3(wasrequests>=2.25), andsslcontext>=2.1.0. Below requests 2.32.3 a caller'sssl_contextwas silently ignored, urllib3 1.26 raises a bareValueErrorfor an https IP-literal host, and urllib3 up to 2.2 leaks the socket of a failed TLS handshake through an https proxy. On Python 3.9, botocore pinsurllib3<1.27, sosyshttp[session]cannot share an environment with it there. -
The header says that jars, tokens,
_PYhooks andauth_providerapply tosession.requestand not to a baresession.send(), which gets the proxy decision and TLS only. -
new_session()no longer callsurllib3.disable_warnings, which silencedInsecureRequestWarningfor the whole process; withverify=Falseurllib3 warns as it does for any requests session. The undocumentedsyshttp.warningsmodule is gone.
Removed
syshttp.session(new_session()and itsrequestsadapter), thesessionextra (and itsrequestsandurllib3floors;requestsstays a development dependency for the tests ofsyshttp.autopatch), and the helpers of the oldsyshttp.fetchmodule:request_text,request_json,request_with_reauth,request_text_autoandrequest_json_auto. Their replacements areHTTPClientandfetch(). A per-requestverify=andcert=are gone with them: build a client withverify=or anssl_contextthat holds the identity.cookies.apply_to_session,merge_set_cookie_headersandhost_only_policy, which only arequestssession used, are gone too.proxy.proxy_url,configured_proxy_url,system_proxy,system_proxy_map,first_reachable,PROBE_MAX_AGE,PROBE_TIMEOUT,resolve,proxy_authorization,configured_authorization,userinfo,basic_authorization,strip_auth,redact,prefix_url,should_bypass,bypassed_by,no_proxy_from_env,proxies_from_env,planandProxyPlan:proxy_map()and proxylib'sProxyreplace them.
Changed (earlier)
- The package stands alone as
syshttp. It was previously thehttpliblibrary of the dotagentsnetoverlay, which shipped with a curl-compatible command (httplib.cli,python -m httplib) that is now thecurlikepackage. Imports move fromhttplibtosyshttp, and settings from the dotagents names to the prefixed ones:AGENTS_PROXY→SYSHTTP_PROXY,AGENTS_PROXY_AUTH→SYSHTTP_PROXY_AUTH,AGENTS_PROXY_AUTH_HEADER→SYSHTTP_PROXY_AUTH_HEADER,AGENTS_PROXY_TYPE→SYSHTTP_PROXY_TYPE,AGENTS_HOME→SYSHTTP_HOME,NET_HOOKS_<KEY>→SYSHTTP_HOOKS_<KEY>,NET_HOOK_*→SYSHTTP_HOOK_*. A tool's own namespace is a prefix:SYSHTTP_PREFIX=AGENTS_readsAGENTS_SYSHTTP_PROXY,AGENTS_SYSHTTP_HOOKS_<KEY>and so on. - The system's TLS client by default:
tls.new_os_context()(and soos_ssl_context()and every session withverify=True) returnssslcontext.system_context()(sslcontext 2 is a new dependency). Certificates are verified by the operating system on Python 3.10+ (CryptoAPI on Windows, the Security framework on macOS, OpenSSL over the system store elsewhere), by OpenSSL over the default store on 3.9, and a server that asks for a client certificate gets one from the OS store, chosen as a browser chooses.new_os_context(client_certificates=False)offers none, for a caller loading its own. Acafile/capathstill replaces the store with a plainsslcontext. - A session without caller
proxiesdecides the proxy for every request and every redirect hop, from the environment as it is then and the system's answer for that URL. A direct decision is final: requests'trust_envcannot re-add a proxy.adapter.route(url)shows the decision. - Without
SYSHTTP_HOME, the jars live in the per-user state directory instead of the dotagents store directory. proxy.resolve()andproxy.proxy_authorization(): the keywordenv: boolis nowconfigured: bool.hooks.hooks_from_env(),matching(),skipped()andcall_py_hooks()takeenv: HTTPEnv(keyword-only) instead of a mappingenviron.hooks.PREFIX,SKIP_ENV,URL_ENV,KEY_ENVandCURL_ENVare replaced by the setting namesHOOKS,HOOK_SKIP,HOOK_URL,HOOK_KEYandHOOK_CURL.proxy.AUTH_VARS,AUTH_HEADER_VARandTYPE_VARare replaced by the setting namesPROXY_AUTH,PROXY_AUTH_HEADERandPROXY_TYPE.
Fixed
- A by-host cookie file keeps a cookie its host set for a parent domain
(
Domain=example.comfromwww.example.com); it was never saved. - The proxy credential reaches a proxy whose host is configured in upper
case. requests lower-cases the proxy URL it hands to
proxy_headers, so the credential used to be dropped and the proxy answered 407. Set-Cookieis read as RFC 6265 section 5.2 reads it, not throughhttp.cookies.SimpleCookie: a value with a space or JSON is kept, an unknown attribute (Priority,Partitioned) is no longer a cookie of its own,Max-AgebeatsExpires, and a header with noname=valueis skipped. ADomainwithout a leading dot covers subdomains. A cookie with noPathgets the directory of the request path (set_cookie_specs(..., request_path=)).- A session holds one copy of each cookie.
merge_set_cookie_headersno longer re-sets what requests already stored (it used to add a second copy with path/and a dotless domain); it applies only the deletions requests misses, for redirect hops too. - A cookie set without
Domainstays on its host: rows loaded from a jar and cookies a server sets are no longer sent to subdomains (new_sessionusescookies.host_only_policy()). - A cookie for a single-label host (
localhost,wiki) is loaded from its jar file, and is saved under its own name instead oflocalhost.local. FileCookieJarkeepsHttpOnlyand the include-subdomains flag.load_netscapekeeps a row whose value is empty, and an expiry written with a non-ASCII digit is a session cookie instead of aValueError.retry_afterreturnsNonefor such a value, and reads an HTTP date written without a zone (asctime) as UTC.- A cookie expiry beyond 400 days is cut to 400 days.
- A hook whose
_PY/_CURLtarget is blank is ignored instead of failing every request, andHTTPEnv.familystrips values asgetdoes. - Hook KEYs are compared without regard to case:
HOOK_SKIP=mykeydid not suppressMYKEY, the form Windows holds.Hook.keyandskipped()are upper-case. - A
<file>.py:callablehook's module is loaded once per process, not on every request (its module-level state was reset each time), and two files with one name no longer share a module name. request_text_autoandrequest_json_autoretry a 408 by default, asnew_sessiondoes (their own list had dropped it), and close the session they built instead of leaking its connection pool. The five fetch helpers have docstrings.new_session(user_agent=...)sets the User-Agent; the defaultpython-requests/xwas sent whatever was given.- The guards that keep a hook or
auth_provider.ensurefrom running again for its own requests are per thread: while one thread was inside a hook, the other threads' requests skipped hooks andensure. - A request-level
verify=(False, or a bundle) besidessl_context, which was silently ignored, raisesValueErroras the same value does at session level.CA_BUNDLEis not a request-level value, and a session with anssl_contextdoes not consult it. - The import patch replaces requests 2.32.3's own default context (a certifi bundle) in a pool, as it does for the later versions that leave it out.
- Token and cookie files are replaced whole by a rename, so a reader never sees an empty or half-written file and concurrent savers no longer truncate each other's file.
SYSHTTP_HOME=~/jarsis expanded instead of creating a directory named~.- Jar files are written with LF on Windows too, as curl writes them.
- A session whose cookie jar cannot be read (an error while iterating
session.cookies) no longer saves an empty list over the host's cookies, and writes a host's file only when its cookies changed instead of after every response.
Security
- The client closes six gaps found in review. A
Set-Cookiewith a control character in it is ignored whole (RFC 6265bis 5.6),load_netscapeends a line at a newline only (it ended one at\x0b,\x0c,\x1c-\x1eand\x85too, so a value could become a whole row for another domain),save_netscapewrites no row with a control character or a tab in a field, and a by-host jar file gives the client only the rows that cover its host or group. A plain-http response can no longer set a Secure cookie, or set, replace or delete one that is stored (RFC 6265bis 5.7; the loopback hosts count as secure, as in curl). A redirect to another scheme, host or port drops a caller'sCookieandProxy-Authorizationheader withAuthorization.HTTPTransport.send(stream=False)andHTTPResponse.buffered()hold a decoded reply toMAX_REPLY_BYTES(a kilobyte of gzip decoded to tens of megabytes), and the too-large error names the URL as scheme, host, port and path, without its query. Anhttps:origin is no longer sent through a plainprefix+httpgateway, which would carry its tokens and Secure cookies in clear text: the entry is skipped, and with nothing else left the request raisesproxylib.UnsupportedProxyError. - A
Set-Cookiecan no longer name a domain its host does not own. Any host could set or delete another host's cookie (Domain=bank.examplefromevil.example) or one for a top level domain; such a header is now skipped on a set and on a deletion, in the session, inFileCookieJarand inset_cookie_specs(which curlike's-cuses). A public suffix is recognised without the Public Suffix List (a name with no dot, or aco.uk-style registry). - Token and cookie files are created
0600, and the directories this library creates0700, instead of world-readable under a usual umask. - A client certificate passed as
cert=is no longer loaded into the shared OS context (or into the caller'sssl_context), where every later session, and every other user of that context, presented it too. It gets a context of the session's own;cert=besidessl_contextraisesValueError. - A URL-prefix gateway is spoken to with
proxy_ssl_context(or the system's verifier), no longer with thessl_contextand client certificate meant for the origin. - A token, or a proxy credential, containing CR, LF or NUL raises a
ValueErrorthat names the jar key or variable instead of requests'InvalidHeadercarrying the whole secret. - No setting can be chosen by a CGI client unless the program reads the
unprefixed names: syshttp's own all start
SYSHTTP_, which a request header never becomes, and a CGI program sets a prefix list without the empty entry so thatHTTP_PROXYandHTTP_PROXY_AUTHare not read. - A failing hook's message names its URL as scheme, host, port and path, no
longer with the userinfo, query and fragment (
user:pass@,?access_token=).