syshttp.client
A synchronous HTTP client on the standard library. syshttp.fetch is
syshttp.client.fetch.
HTTPClient(transport=...) sends through any object with send() and
close() (the Transport protocol), such as a transport built on requests
or httpx, or a fake in a test. The client owns redirects, credentials,
cookies, tokens, hooks, retries and the deadline; the transport owns routing,
TLS verification, connection reuse and content decoding. A transport written
elsewhere therefore handles proxies and TLS itself, as its library does;
prepare_request() gives it the checks and body framing of syshttp's own, and
SYSHTTP_DEFAULT_TRANSPORT makes it the default (see Settings).
syshttp.client
An HTTP client on the standard library, with a replaceable transport.
HTTPClient sends a request through a transport (an object with send() and
close(): the default HTTPTransport on http.client, or one written
against the Transport protocol) and follows redirects under one rule for
credentials: Authorization, Cookie and Proxy-Authorization go to the
origin they were given for and to no other scheme, host or port. A reply comes
back as an HTTPResponse.
Importing this package loads http.client and ssl. Nothing here needs a
third-party library; requests in particular is not used.
CA_BUNDLE = 'CA_BUNDLE'
module-attribute
The setting naming a CA bundle (a PEM file) that replaces the system's trust store for origins when verification is on and the caller gave no context.
COOKIE_HOSTS = 'SYSHTTP_COOKIE_HOSTS'
module-attribute
The setting declaring hosts that share cookies: hosts split by ,, groups
by ;.
DEFAULT_TIMEOUT = (10, 60)
module-attribute
(connect, read) timeout, in seconds, a client sends with every request
unless it is given timeout (HTTPClient(timeout=...) or per request);
timeout=None waits without a limit.
DEFAULT_TRANSPORT = 'SYSHTTP_DEFAULT_TRANSPORT'
module-attribute
The setting naming the transport of a client given none: an object reference
module:attr or <file>.py:attr (a dotted attr is allowed). The object is
the transport when it has send(); otherwise it is called with no arguments
to build one (a class, a factory). It names code to import: never let an
untrusted party set it.
HTTPClient(transport=None, *, headers=None, timeout=DEFAULT_TIMEOUT, deadline=None, user_agent=None, env=None, proxymap=None, ssl_context=None, proxy_ssl_context=None, verify=True, cookies=False, tokens=False, auth_provider=None, retries=3, backoff=1, status_forcelist=TRANSIENT_STATUSES, cookie_hosts=None)
A transport with the settings every request is sent with, the rules for what happens after a redirect, and the state that belongs to a client.
transport is any object with a send() method as HTTPTransport.send();
one it is given stays the caller's. Without one the client builds the
transport the SYSHTTP_DEFAULT_TRANSPORT setting names, else an
HTTPTransport, when it is made, and closes it with close() or at the
end of a with block. proxymap, ssl_context, proxy_ssl_context and
verify configure that HTTPTransport and are ValueError beside a
transport of your own; env (default HTTPEnv()) is where the client and
its transport read their settings. headers are sent with every request,
request-specific headers merged over them by name, over a User-Agent of
user_agent (syshttp/<version> by default) and Accept: */*.
Every request gets timeout=DEFAULT_TIMEOUT ((10, 60) seconds) unless
timeout here or in the call says otherwise (None waits without limit).
timeout bounds each wait; deadline (seconds, None for no limit, the
default) bounds the whole request: redirects, retries and the reading of
the body included, each wait's timeout is held to the time that remains,
and TimeoutError is raised once it has passed. A read already blocked can
overrun it by at most the timeout of that wait.
The client, not the transport, follows redirects. GET, HEAD and
OPTIONS follow up to 30; a redirect that changes the scheme, host or
port drops Authorization, Cookie and Proxy-Authorization (and nothing
else; the jar's cookies are chosen again for each hop). Any other method is sent
without following: a 307 or 308 (for PROPFIND also a 301, 302 or 303) to
the same scheme, host and port is sent once more with the same method and
body, and any other 3xx, a redirect to another origin or a second redirect
raises OSError(EIO).
State, chosen by the host of the URL asked for (the origin), never by a
proxy's or a gateway's. A client keeps the cookies its responses set, in
memory and, with cookies, in a jar: True is a file jar under
jar.store_root() by host, a string one key for every host, a jar object
is used as given. cookie_hosts (default SYSHTTP_COOKIE_HOSTS) declares
groups of hosts that share cookies. tokens is read the same way and
becomes a per-request Authorization (Bearer <token>, or verbatim when
it names a scheme) that follows no redirect to another origin. In
request() (not in redirect()): auth_provider.ensure(client, url) runs
first, its errors swallowed; the SYSHTTP_HOOKS_<KEY>_PY hooks run on the
caller's URL, and a request a hook makes through the client runs none of
them again (the guard is per thread); a status in status_forcelist, or a
failed connection (ConnectionError), is retried up to retries times for
a method that is safe to send twice, after backoff seconds doubled each
time and never less than the Retry-After of the reply, and a status that
outlasts the retries is returned, not raised.
Source code in src/syshttp/client/_client.py
106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 | |
transport
property
The transport: the one the client was given, or the one it made
(again, after close()). Read once per request.
close()
Closes the transport this client built for itself; one it was given stays the caller's. Harmless when called again.
Source code in src/syshttp/client/_client.py
182 183 184 185 186 187 188 189 190 | |
redirect(method, url, resp, *, headers=None, stream=False, timeout=_UNSET, deadline=_UNSET, auth=None)
Send method to where resp, the answer to a request for url,
redirects to, and follow what it redirects to in turn. The request
carries what url is sent with, except that Authorization, Cookie
and Proxy-Authorization are left off whenever it goes to another
scheme, host or port than url's. A
redirect with no usable Location is OSError(EIO). Cookies apply to
every hop; the token, hooks and retries belong to request().
Source code in src/syshttp/client/_client.py
389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 | |
request(method, url, *, data=None, headers=None, stream=False, allow_redirects=None, timeout=_UNSET, deadline=_UNSET, auth=None)
Send method to url and return the final response. A data that
is not bytes (a file, an iterable) cannot be sent twice, so it is not
re-sent after a redirect, a dropped connection or a retry.
allow_redirects=False returns the first answer of a GET, HEAD or
OPTIONS as it is. auth is a (user, password) pair sent as
Authorization: Basic, or a str sent as the Authorization value
("Bearer <token>"), to the origin of url unless a header names an
Authorization; it beats a token of the client. ValueError for a URL
(credentials in it included) or a header with a control character,
before anything is sent.
The auth provider, the hooks, the token and the retries of the client
apply here. A hook is called as hook(client, method, url, kwargs) with
the keyword arguments given (data, headers, stream,
allow_redirects, timeout, deadline, auth), which it may change
in place, and may return a response to be used instead of sending.
With stream=False the body is read before this returns, and a decoded
body over MAX_REPLY_BYTES is OSError(EFBIG); with stream=True the
caller reads it, and closes the response.
Source code in src/syshttp/client/_client.py
319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 | |
HTTPResponse(status, reason, headers, url, body=b'', *, decode=True)
One response: status, reason, headers (a read-only mapping that
ignores the case of a name), url (the URL the request went to) and the
body, read with read() or iter_bytes(). A context manager that closes.
The body is decoded when the response says it is gzip or deflate
encoded, unless the request set its own Accept-Encoding; headers are
the headers as received, so Content-Length counts the encoded bytes. A
body that ends before the length the server stated is OSError(EIO), never
a shorter body that reads as complete.
A transport builds one with body as bytes, or as an object with
read(amount) -> bytes (empty at the end) and close(), and
decode=False for a body it already decoded or that was not asked for
in a coding.
Source code in src/syshttp/client/_response.py
135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 | |
content
property
The rest of the decoded body, read on first use and kept, so that
content, text() and json() agree however often they are used. A
body longer than MAX_REPLY_BYTES is OSError(EFBIG). Bytes already
taken with read() or iter_bytes() are not in it.
buffered(limit=None)
Reads the rest of the body into memory, closes the connection and
returns this response, which can then be read after it is closed. A
transport returns its response this way when send() is called with
stream=False. A body that decodes to more than limit bytes (default
MAX_REPLY_BYTES) is OSError(EFBIG), and its connection is closed.
Source code in src/syshttp/client/_response.py
279 280 281 282 283 284 285 286 | |
close()
Releases the connection. Harmless when called again. A body that was not read to its end is abandoned, and its connection is closed.
Source code in src/syshttp/client/_response.py
272 273 274 275 276 277 | |
iter_bytes(chunk_size=CHUNK)
The rest of the body in pieces of at most chunk_size bytes, each
as soon as it has arrived.
Source code in src/syshttp/client/_response.py
222 223 224 225 226 227 228 229 230 231 | |
json()
content parsed as JSON; None for an empty body. A body that is
not JSON is ResponseError (a ValueError) naming the status.
Source code in src/syshttp/client/_response.py
256 257 258 259 260 261 262 263 264 265 | |
read(size=-1)
Up to size bytes of the decoded body, fewer only at its end; the
whole rest of it when size is negative or None.
Source code in src/syshttp/client/_response.py
202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 | |
text(encoding=None, errors='replace')
content decoded with encoding, else the charset of the
Content-Type header, else UTF-8; characters that do not decode are
replaced unless errors says otherwise.
Source code in src/syshttp/client/_response.py
245 246 247 248 249 250 251 252 253 254 | |
HTTPStatusError(response)
Bases: Exception
A reply with an error status (4xx or 5xx). response is the reply.
Source code in src/syshttp/client/_status.py
16 17 18 | |
HTTPTransport(*, proxymap=None, env=None, ssl_context=None, proxy_ssl_context=None, verify=True)
Sends HTTP requests on the standard library: http.client over a socket,
with TLS from ssl, and no third-party code.
send() makes exactly one request: it follows no redirect, adds no
credentials of the caller's and retries only as described below. A
transport is a context manager, and close() closes every connection it
holds.
Routing. Each request asks proxymap (a proxylib proxy map; default
proxy.proxy_map(env=env): SYSHTTP_PROXY, the *_PROXY variables,
NO_PROXY, then the system's configuration) which entries it may use, and
takes the first that accepts a connection (proxylib.Probed; with no entry
reachable and no direct one, proxylib.NoReachableProxyError). A direct
entry connects to the origin. An http: or https: proxy gets an
absolute-form request for an http: origin and a CONNECT for an https:
one; its headers (a credential) go to it alone, in the CONNECT or on the
plain-http request, never through the tunnel. A prefix+http(s) gateway is
asked for <gateway><path><URL> with its credential as a header of that
request, and the response keeps the URL asked for. SOCKS entries are not
spoken (proxylib.UnsupportedProxyError when nothing else is offered). A
map with no opinion for a URL, {} included, means a direct connection.
TLS. https: origins are verified against ssl_context when given,
else the CA bundle named by the CA_BUNDLE setting, else the system's
context (tls.os_ssl_context()). verify=False is the one way to turn
verification off, verify=<path> names a bundle, and an ssl_context
beside either is ValueError. An https: proxy or gateway is spoken to
with proxy_ssl_context, else the system's context.
Each thread keeps one idle connection per route (at most 8), which a later
request reuses unless the server has closed it or sent bytes on it since;
the connections of a thread are closed when it ends. A
request that fails before any response byte on a reused connection because
the server had closed it is sent once more on a new connection, but only
if it is repeatable (GET, HEAD, OPTIONS, PUT, DELETE,
PROPFIND) and its body is None or in memory. Any other request reuses a
connection only if it carried a response within the last second.
Source code in src/syshttp/client/_transport.py
298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 | |
close()
Closes every connection, idle or in use. Harmless when called again; a later request connects afresh.
Source code in src/syshttp/client/_transport.py
329 330 331 332 | |
send(method, url, *, headers, body=None, timeout, stream=False)
One request, one response.
headers are sent as given; Accept-Encoding: gzip, deflate is added
(and a coded body decoded) unless they name an Accept-Encoding.
A body whose length is known without reading it goes under a
Content-Length, any other chunked; a Content-Length in headers
is used as it is. timeout is seconds for the connection and for each wait on the
server: a number, a (connect, read) pair, or None to wait for ever.
With stream=False the body has been read when this returns (a body
that decodes to more than MAX_REPLY_BYTES is OSError(EFBIG)); with
stream=True it is read from the response, which must be closed.
Raises ValueError before anything is sent for a header or URL with
a control character, a URL with credentials or another scheme;
TimeoutError, ConnectionError and ssl.SSLError for the network;
OSError(EIO) for a reply that is not valid HTTP or ends early. The
route adds proxylib.NoReachableProxyError,
proxylib.UnsupportedProxyError and, for a setting that cannot be used,
ConfigurationError.
Source code in src/syshttp/client/_transport.py
334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 | |
Headers(pairs=())
Bases: Mapping
The headers of a response: a read-only mapping whose names compare
without regard to case. A name sent more than once reads as its values
joined with ", " (RFC 9110 5.3); get_all() returns them separately.
Source code in src/syshttp/client/_headers.py
26 27 28 29 30 31 32 33 34 35 36 | |
get_all(name)
Every value sent for name, in order.
Source code in src/syshttp/client/_headers.py
50 51 52 53 | |
PreparedRequest
Bases: NamedTuple
A request that passed the checks of the transport contract.
headers hold text values, with the Content-Length of a body whose
length is known (0 for a PATCH, POST or PUT without a body) and an
Accept-Encoding of gzip, deflate unless the caller named one. body is
None, bytes, an object that sends exactly the Content-Length bytes
when it is read or iterated, or an iterable of bytes to send chunked.
timeout is (connect, read). decode says whether the response decodes
a gzip or deflate body: build it with HTTPResponse(...,
decode=decode).
Transport
Bases: Protocol
What HTTPClient sends through: one request, one response, no redirect,
no credentials added.
send() takes the arguments prepare_request() checks and returns an
HTTPResponse (stream=False: its body already read; stream=True: the
caller closes it); close() releases whatever the transport holds. The
client does the redirects, cookies, tokens, hooks, retries and deadline
above it. Routing (the proxy map, CONNECT, gateways), TLS verification,
connection reuse and decoding a gzip or deflate body are the
transport's: HTTPTransport does them with syshttp's proxy map and the
system's TLS, and a transport on another library brings that library's.
drop_body(resp, limit=None)
Gives up the body of a reply nobody looks at: it is read, up to limit
bytes (default MAX_REPLY_BYTES), so the connection can serve the next
request, and resp is closed (which closes the connection when the body
was longer). A failure while reading it is not the caller's concern: the
status was already used.
Source code in src/syshttp/client/_status.py
36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | |
fetch(url, method='GET', *, headers=None, data=None, json=None, timeout=DEFAULT_TIMEOUT, client=None, transport=None, reauth=None, **client_options)
Send method to url and return the response with its body read whole
(MAX_REPLY_BYTES bounds it), so the caller takes what it needs from it:
.status, .headers, .content, .text() or .json(). A status is never
an exception: raise_for_status() is the caller's choice.
data is bytes, text, a mapping (form-encoded) or a binary file; json a
value sent as JSON (None sends none: use data=b"null" for that);
giving both is ValueError. timeout is seconds or a (connect, read)
pair. The request goes through client when given (left open), else
through a new HTTPClient(transport, **client_options) closed after the
call: transport is any object implementing the transport protocol (None:
the SYSHTTP_DEFAULT_TRANSPORT setting's, else syshttp's own), and
client_options are the other keywords of HTTPClient (env, proxymap,
ssl_context, retries, ...). A client beside transport or
client_options is ValueError: a given client already has them.
reauth is called as reauth(client, url) (an object with
ensure(client, url), an AuthProvider, is called through that) once after
a 401 or 403, and the request is sent one more time; its errors are the
caller's. The body must then be one that can be sent twice (bytes, text, a
mapping or json).
Source code in src/syshttp/client/_fetch.py
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 | |
json_body(resp, what, limit=None)
The JSON document in the body of resp (at most limit bytes, default
MAX_REPLY_BYTES, else OSError(EFBIG) naming what); ValueError when
it is not one.
Source code in src/syshttp/client/_status.py
55 56 57 58 59 | |
peer_text(text, limit=120)
text that came from the other end (a reason phrase, a Location, a
header value) as an error message may show it: characters a terminal or a
log would act on (controls, line and bidirectional separators) escaped, and
cut to limit characters.
Source code in src/syshttp/client/_headers.py
60 61 62 63 64 65 66 67 68 69 70 | |
prepare_request(method, url, *, headers, body=None, timeout)
The arguments of send() as the contract reads them.
method is upper-cased. Raises ValueError, before anything is sent, for
a method or header name that is not an HTTP token, a header value or URL
with a control character, a URL with credentials or another scheme, a
Content-Length that is not a count of bytes or exceeds the body, and a
timeout that is not a positive number, None or a pair of them;
TypeError for a str body. None of the messages quotes the URL or a
header value. The fragment is dropped from url.
Source code in src/syshttp/client/_prepare.py
43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 | |
raise_for_status(resp)
Raises HTTPStatusError for a reply with status 400 or above.
Source code in src/syshttp/client/_status.py
21 22 23 24 | |
read_limited(resp, what, limit=None)
The decoded body of resp, which must be at most limit bytes (default
MAX_REPLY_BYTES): OSError(EFBIG) naming what otherwise, with the
connection closed.
Source code in src/syshttp/client/_status.py
27 28 29 30 31 32 33 | |
split_userinfo(url)
Split the userinfo out of an absolute URL: (url_without_userinfo,
(user, password) | None), both parts percent-decoded to the octets they
stand for. Credentials inside a request URL would end up in
HTTPResponse.url, in error text and in redirect handling; they are sent
in an Authorization header instead.
Source code in src/syshttp/client/_follow.py
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 | |