Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/httpx/_client.py: 26%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1from __future__ import annotations
3import datetime
4import enum
5import logging
6import time
7import typing
8import warnings
9from contextlib import asynccontextmanager, contextmanager
10from types import TracebackType
12from .__version__ import __version__
13from ._auth import Auth, BasicAuth, FunctionAuth
14from ._config import (
15 DEFAULT_LIMITS,
16 DEFAULT_MAX_REDIRECTS,
17 DEFAULT_TIMEOUT_CONFIG,
18 Limits,
19 Proxy,
20 Timeout,
21)
22from ._decoders import SUPPORTED_DECODERS
23from ._exceptions import (
24 InvalidURL,
25 RemoteProtocolError,
26 TooManyRedirects,
27 request_context,
28)
29from ._models import Cookies, Headers, Request, Response
30from ._status_codes import codes
31from ._transports.base import AsyncBaseTransport, BaseTransport
32from ._transports.default import AsyncHTTPTransport, HTTPTransport
33from ._types import (
34 AsyncByteStream,
35 AuthTypes,
36 CertTypes,
37 CookieTypes,
38 HeaderTypes,
39 ProxyTypes,
40 QueryParamTypes,
41 RequestContent,
42 RequestData,
43 RequestExtensions,
44 RequestFiles,
45 SyncByteStream,
46 TimeoutTypes,
47)
48from ._urls import URL, QueryParams
49from ._utils import URLPattern, get_environment_proxies
51if typing.TYPE_CHECKING:
52 import ssl # pragma: no cover
54__all__ = ["USE_CLIENT_DEFAULT", "AsyncClient", "Client"]
56# The type annotation for @classmethod and context managers here follows PEP 484
57# https://www.python.org/dev/peps/pep-0484/#annotating-instance-and-class-methods
58T = typing.TypeVar("T", bound="Client")
59U = typing.TypeVar("U", bound="AsyncClient")
62def _is_https_redirect(url: URL, location: URL) -> bool:
63 """
64 Return 'True' if 'location' is a HTTPS upgrade of 'url'
65 """
66 if url.host != location.host:
67 return False
69 return (
70 url.scheme == "http"
71 and _port_or_default(url) == 80
72 and location.scheme == "https"
73 and _port_or_default(location) == 443
74 )
77def _port_or_default(url: URL) -> int | None:
78 if url.port is not None:
79 return url.port
80 return {"http": 80, "https": 443}.get(url.scheme)
83def _same_origin(url: URL, other: URL) -> bool:
84 """
85 Return 'True' if the given URLs share the same origin.
86 """
87 return (
88 url.scheme == other.scheme
89 and url.host == other.host
90 and _port_or_default(url) == _port_or_default(other)
91 )
94class UseClientDefault:
95 """
96 For some parameters such as `auth=...` and `timeout=...` we need to be able
97 to indicate the default "unset" state, in a way that is distinctly different
98 to using `None`.
100 The default "unset" state indicates that whatever default is set on the
101 client should be used. This is different to setting `None`, which
102 explicitly disables the parameter, possibly overriding a client default.
104 For example we use `timeout=USE_CLIENT_DEFAULT` in the `request()` signature.
105 Omitting the `timeout` parameter will send a request using whatever default
106 timeout has been configured on the client. Including `timeout=None` will
107 ensure no timeout is used.
109 Note that user code shouldn't need to use the `USE_CLIENT_DEFAULT` constant,
110 but it is used internally when a parameter is not included.
111 """
114USE_CLIENT_DEFAULT = UseClientDefault()
117logger = logging.getLogger("httpx")
119USER_AGENT = f"python-httpx/{__version__}"
120ACCEPT_ENCODING = ", ".join(
121 [key for key in SUPPORTED_DECODERS.keys() if key != "identity"]
122)
125class ClientState(enum.Enum):
126 # UNOPENED:
127 # The client has been instantiated, but has not been used to send a request,
128 # or been opened by entering the context of a `with` block.
129 UNOPENED = 1
130 # OPENED:
131 # The client has either sent a request, or is within a `with` block.
132 OPENED = 2
133 # CLOSED:
134 # The client has either exited the `with` block, or `close()` has
135 # been called explicitly.
136 CLOSED = 3
139class BoundSyncStream(SyncByteStream):
140 """
141 A byte stream that is bound to a given response instance, and that
142 ensures the `response.elapsed` is set once the response is closed.
143 """
145 def __init__(
146 self, stream: SyncByteStream, response: Response, start: float
147 ) -> None:
148 self._stream = stream
149 self._response = response
150 self._start = start
152 def __iter__(self) -> typing.Iterator[bytes]:
153 for chunk in self._stream:
154 yield chunk
156 def close(self) -> None:
157 elapsed = time.perf_counter() - self._start
158 self._response.elapsed = datetime.timedelta(seconds=elapsed)
159 self._stream.close()
162class BoundAsyncStream(AsyncByteStream):
163 """
164 An async byte stream that is bound to a given response instance, and that
165 ensures the `response.elapsed` is set once the response is closed.
166 """
168 def __init__(
169 self, stream: AsyncByteStream, response: Response, start: float
170 ) -> None:
171 self._stream = stream
172 self._response = response
173 self._start = start
175 async def __aiter__(self) -> typing.AsyncIterator[bytes]:
176 async for chunk in self._stream:
177 yield chunk
179 async def aclose(self) -> None:
180 elapsed = time.perf_counter() - self._start
181 self._response.elapsed = datetime.timedelta(seconds=elapsed)
182 await self._stream.aclose()
185EventHook = typing.Callable[..., typing.Any]
188class BaseClient:
189 def __init__(
190 self,
191 *,
192 auth: AuthTypes | None = None,
193 params: QueryParamTypes | None = None,
194 headers: HeaderTypes | None = None,
195 cookies: CookieTypes | None = None,
196 timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
197 follow_redirects: bool = False,
198 max_redirects: int = DEFAULT_MAX_REDIRECTS,
199 event_hooks: None | (typing.Mapping[str, list[EventHook]]) = None,
200 base_url: URL | str = "",
201 trust_env: bool = True,
202 default_encoding: str | typing.Callable[[bytes], str] = "utf-8",
203 ) -> None:
204 event_hooks = {} if event_hooks is None else event_hooks
206 self._base_url = self._enforce_trailing_slash(URL(base_url))
208 self._auth = self._build_auth(auth)
209 self._params = QueryParams(params)
210 self.headers = Headers(headers)
211 self._cookies = Cookies(cookies)
212 self._timeout = Timeout(timeout)
213 self.follow_redirects = follow_redirects
214 self.max_redirects = max_redirects
215 self._event_hooks = {
216 "request": list(event_hooks.get("request", [])),
217 "response": list(event_hooks.get("response", [])),
218 }
219 self._trust_env = trust_env
220 self._default_encoding = default_encoding
221 self._state = ClientState.UNOPENED
223 @property
224 def is_closed(self) -> bool:
225 """
226 Check if the client being closed
227 """
228 return self._state == ClientState.CLOSED
230 @property
231 def trust_env(self) -> bool:
232 return self._trust_env
234 def _enforce_trailing_slash(self, url: URL) -> URL:
235 if url.raw_path.endswith(b"/"):
236 return url
237 return url.copy_with(raw_path=url.raw_path + b"/")
239 def _get_proxy_map(
240 self, proxy: ProxyTypes | None, allow_env_proxies: bool
241 ) -> dict[str, Proxy | None]:
242 if proxy is None:
243 if allow_env_proxies:
244 return {
245 key: None if url is None else Proxy(url=url)
246 for key, url in get_environment_proxies().items()
247 }
248 return {}
249 else:
250 proxy = Proxy(url=proxy) if isinstance(proxy, (str, URL)) else proxy
251 return {"all://": proxy}
253 @property
254 def timeout(self) -> Timeout:
255 return self._timeout
257 @timeout.setter
258 def timeout(self, timeout: TimeoutTypes) -> None:
259 self._timeout = Timeout(timeout)
261 @property
262 def event_hooks(self) -> dict[str, list[EventHook]]:
263 return self._event_hooks
265 @event_hooks.setter
266 def event_hooks(self, event_hooks: dict[str, list[EventHook]]) -> None:
267 self._event_hooks = {
268 "request": list(event_hooks.get("request", [])),
269 "response": list(event_hooks.get("response", [])),
270 }
272 @property
273 def auth(self) -> Auth | None:
274 """
275 Authentication class used when none is passed at the request-level.
277 See also [Authentication][0].
279 [0]: /quickstart/#authentication
280 """
281 return self._auth
283 @auth.setter
284 def auth(self, auth: AuthTypes) -> None:
285 self._auth = self._build_auth(auth)
287 @property
288 def base_url(self) -> URL:
289 """
290 Base URL to use when sending requests with relative URLs.
291 """
292 return self._base_url
294 @base_url.setter
295 def base_url(self, url: URL | str) -> None:
296 self._base_url = self._enforce_trailing_slash(URL(url))
298 @property
299 def headers(self) -> Headers:
300 """
301 HTTP headers to include when sending requests.
302 """
303 return self._headers
305 @headers.setter
306 def headers(self, headers: HeaderTypes) -> None:
307 client_headers = Headers(
308 {
309 b"Accept": b"*/*",
310 b"Accept-Encoding": ACCEPT_ENCODING.encode("ascii"),
311 b"Connection": b"keep-alive",
312 b"User-Agent": USER_AGENT.encode("ascii"),
313 }
314 )
315 client_headers.update(headers)
316 self._headers = client_headers
318 @property
319 def cookies(self) -> Cookies:
320 """
321 Cookie values to include when sending requests.
322 """
323 return self._cookies
325 @cookies.setter
326 def cookies(self, cookies: CookieTypes) -> None:
327 self._cookies = Cookies(cookies)
329 @property
330 def params(self) -> QueryParams:
331 """
332 Query parameters to include in the URL when sending requests.
333 """
334 return self._params
336 @params.setter
337 def params(self, params: QueryParamTypes) -> None:
338 self._params = QueryParams(params)
340 def build_request(
341 self,
342 method: str,
343 url: URL | str,
344 *,
345 content: RequestContent | None = None,
346 data: RequestData | None = None,
347 files: RequestFiles | None = None,
348 json: typing.Any | None = None,
349 params: QueryParamTypes | None = None,
350 headers: HeaderTypes | None = None,
351 cookies: CookieTypes | None = None,
352 timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
353 extensions: RequestExtensions | None = None,
354 ) -> Request:
355 """
356 Build and return a request instance.
358 * The `params`, `headers` and `cookies` arguments
359 are merged with any values set on the client.
360 * The `url` argument is merged with any `base_url` set on the client.
362 See also: [Request instances][0]
364 [0]: /advanced/clients/#request-instances
365 """
366 url = self._merge_url(url)
367 headers = self._merge_headers(headers)
368 cookies = self._merge_cookies(cookies)
369 params = self._merge_queryparams(params)
370 extensions = {} if extensions is None else extensions
371 if "timeout" not in extensions:
372 timeout = (
373 self.timeout
374 if isinstance(timeout, UseClientDefault)
375 else Timeout(timeout)
376 )
377 extensions = dict(**extensions, timeout=timeout.as_dict())
378 return Request(
379 method,
380 url,
381 content=content,
382 data=data,
383 files=files,
384 json=json,
385 params=params,
386 headers=headers,
387 cookies=cookies,
388 extensions=extensions,
389 )
391 def _merge_url(self, url: URL | str) -> URL:
392 """
393 Merge a URL argument together with any 'base_url' on the client,
394 to create the URL used for the outgoing request.
395 """
396 merge_url = URL(url)
397 if merge_url.is_relative_url:
398 # To merge URLs we always append to the base URL. To get this
399 # behaviour correct we always ensure the base URL ends in a '/'
400 # separator, and strip any leading '/' from the merge URL.
401 #
402 # So, eg...
403 #
404 # >>> client = Client(base_url="https://www.example.com/subpath")
405 # >>> client.base_url
406 # URL('https://www.example.com/subpath/')
407 # >>> client.build_request("GET", "/path").url
408 # URL('https://www.example.com/subpath/path')
409 merge_raw_path = self.base_url.raw_path + merge_url.raw_path.lstrip(b"/")
410 return self.base_url.copy_with(raw_path=merge_raw_path)
411 return merge_url
413 def _merge_cookies(self, cookies: CookieTypes | None = None) -> CookieTypes | None:
414 """
415 Merge a cookies argument together with any cookies on the client,
416 to create the cookies used for the outgoing request.
417 """
418 if cookies or self.cookies:
419 merged_cookies = Cookies(self.cookies)
420 merged_cookies.update(cookies)
421 return merged_cookies
422 return cookies
424 def _merge_headers(self, headers: HeaderTypes | None = None) -> HeaderTypes | None:
425 """
426 Merge a headers argument together with any headers on the client,
427 to create the headers used for the outgoing request.
428 """
429 merged_headers = Headers(self.headers)
430 merged_headers.update(headers)
431 return merged_headers
433 def _merge_queryparams(
434 self, params: QueryParamTypes | None = None
435 ) -> QueryParamTypes | None:
436 """
437 Merge a queryparams argument together with any queryparams on the client,
438 to create the queryparams used for the outgoing request.
439 """
440 if params or self.params:
441 merged_queryparams = QueryParams(self.params)
442 return merged_queryparams.merge(params)
443 return params
445 def _build_auth(self, auth: AuthTypes | None) -> Auth | None:
446 if auth is None:
447 return None
448 elif isinstance(auth, tuple):
449 return BasicAuth(username=auth[0], password=auth[1])
450 elif isinstance(auth, Auth):
451 return auth
452 elif callable(auth):
453 return FunctionAuth(func=auth)
454 else:
455 raise TypeError(f'Invalid "auth" argument: {auth!r}')
457 def _build_request_auth(
458 self,
459 request: Request,
460 auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT,
461 ) -> Auth:
462 auth = (
463 self._auth if isinstance(auth, UseClientDefault) else self._build_auth(auth)
464 )
466 if auth is not None:
467 return auth
469 username, password = request.url.username, request.url.password
470 if username or password:
471 return BasicAuth(username=username, password=password)
473 return Auth()
475 def _build_redirect_request(self, request: Request, response: Response) -> Request:
476 """
477 Given a request and a redirect response, return a new request that
478 should be used to effect the redirect.
479 """
480 method = self._redirect_method(request, response)
481 url = self._redirect_url(request, response)
482 headers = self._redirect_headers(request, url, method)
483 stream = self._redirect_stream(request, method)
484 cookies = Cookies(self.cookies)
485 return Request(
486 method=method,
487 url=url,
488 headers=headers,
489 cookies=cookies,
490 stream=stream,
491 extensions=request.extensions,
492 )
494 def _redirect_method(self, request: Request, response: Response) -> str:
495 """
496 When being redirected we may want to change the method of the request
497 based on certain specs or browser behavior.
498 """
499 method = request.method
501 # https://tools.ietf.org/html/rfc7231#section-6.4.4
502 if response.status_code == codes.SEE_OTHER and method != "HEAD":
503 method = "GET"
505 # Do what the browsers do, despite standards...
506 # Turn 302s into GETs.
507 if response.status_code == codes.FOUND and method != "HEAD":
508 method = "GET"
510 # If a POST is responded to with a 301, turn it into a GET.
511 # This bizarre behaviour is explained in 'requests' issue 1704.
512 if response.status_code == codes.MOVED_PERMANENTLY and method == "POST":
513 method = "GET"
515 return method
517 def _redirect_url(self, request: Request, response: Response) -> URL:
518 """
519 Return the URL for the redirect to follow.
520 """
521 location = response.headers["Location"]
523 try:
524 url = URL(location)
525 except InvalidURL as exc:
526 raise RemoteProtocolError(
527 f"Invalid URL in location header: {exc}.", request=request
528 ) from None
530 # Handle malformed 'Location' headers that are "absolute" form, have no host.
531 # See: https://github.com/encode/httpx/issues/771
532 if url.scheme and not url.host:
533 url = url.copy_with(host=request.url.host)
535 # Facilitate relative 'Location' headers, as allowed by RFC 7231.
536 # (e.g. '/path/to/resource' instead of 'http://domain.tld/path/to/resource')
537 if url.is_relative_url:
538 url = request.url.join(url)
540 # Attach previous fragment if needed (RFC 7231 7.1.2)
541 if request.url.fragment and not url.fragment:
542 url = url.copy_with(fragment=request.url.fragment)
544 return url
546 def _redirect_headers(self, request: Request, url: URL, method: str) -> Headers:
547 """
548 Return the headers that should be used for the redirect request.
549 """
550 headers = Headers(request.headers)
552 if not _same_origin(url, request.url):
553 if not _is_https_redirect(request.url, url):
554 # Strip Authorization headers when responses are redirected
555 # away from the origin. (Except for direct HTTP to HTTPS redirects.)
556 headers.pop("Authorization", None)
558 # Update the Host header.
559 headers["Host"] = url.netloc.decode("ascii")
561 if method != request.method and method == "GET":
562 # If we've switch to a 'GET' request, then strip any headers which
563 # are only relevant to the request body.
564 headers.pop("Content-Length", None)
565 headers.pop("Transfer-Encoding", None)
567 # We should use the client cookie store to determine any cookie header,
568 # rather than whatever was on the original outgoing request.
569 headers.pop("Cookie", None)
571 return headers
573 def _redirect_stream(
574 self, request: Request, method: str
575 ) -> SyncByteStream | AsyncByteStream | None:
576 """
577 Return the body that should be used for the redirect request.
578 """
579 if method != request.method and method == "GET":
580 return None
582 return request.stream
584 def _set_timeout(self, request: Request) -> None:
585 if "timeout" not in request.extensions:
586 timeout = (
587 self.timeout
588 if isinstance(self.timeout, UseClientDefault)
589 else Timeout(self.timeout)
590 )
591 request.extensions = dict(**request.extensions, timeout=timeout.as_dict())
594class Client(BaseClient):
595 """
596 An HTTP client, with connection pooling, HTTP/2, redirects, cookie persistence, etc.
598 It can be shared between threads.
600 Usage:
602 ```python
603 >>> client = httpx.Client()
604 >>> response = client.get('https://example.org')
605 ```
607 **Parameters:**
609 * **auth** - *(optional)* An authentication class to use when sending
610 requests.
611 * **params** - *(optional)* Query parameters to include in request URLs, as
612 a string, dictionary, or sequence of two-tuples.
613 * **headers** - *(optional)* Dictionary of HTTP headers to include when
614 sending requests.
615 * **cookies** - *(optional)* Dictionary of Cookie items to include when
616 sending requests.
617 * **verify** - *(optional)* Either `True` to use an SSL context with the
618 default CA bundle, `False` to disable verification, or an instance of
619 `ssl.SSLContext` to use a custom context.
620 * **http2** - *(optional)* A boolean indicating if HTTP/2 support should be
621 enabled. Defaults to `False`.
622 * **proxy** - *(optional)* A proxy URL where all the traffic should be routed.
623 * **timeout** - *(optional)* The timeout configuration to use when sending
624 requests.
625 * **limits** - *(optional)* The limits configuration to use.
626 * **max_redirects** - *(optional)* The maximum number of redirect responses
627 that should be followed.
628 * **base_url** - *(optional)* A URL to use as the base when building
629 request URLs.
630 * **transport** - *(optional)* A transport class to use for sending requests
631 over the network.
632 * **trust_env** - *(optional)* Enables or disables usage of environment
633 variables for configuration.
634 * **default_encoding** - *(optional)* The default encoding to use for decoding
635 response text, if no charset information is included in a response Content-Type
636 header. Set to a callable for automatic character set detection. Default: "utf-8".
637 """
639 def __init__(
640 self,
641 *,
642 auth: AuthTypes | None = None,
643 params: QueryParamTypes | None = None,
644 headers: HeaderTypes | None = None,
645 cookies: CookieTypes | None = None,
646 verify: ssl.SSLContext | str | bool = True,
647 cert: CertTypes | None = None,
648 trust_env: bool = True,
649 http1: bool = True,
650 http2: bool = False,
651 proxy: ProxyTypes | None = None,
652 mounts: None | (typing.Mapping[str, BaseTransport | None]) = None,
653 timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
654 follow_redirects: bool = False,
655 limits: Limits = DEFAULT_LIMITS,
656 max_redirects: int = DEFAULT_MAX_REDIRECTS,
657 event_hooks: None | (typing.Mapping[str, list[EventHook]]) = None,
658 base_url: URL | str = "",
659 transport: BaseTransport | None = None,
660 default_encoding: str | typing.Callable[[bytes], str] = "utf-8",
661 ) -> None:
662 super().__init__(
663 auth=auth,
664 params=params,
665 headers=headers,
666 cookies=cookies,
667 timeout=timeout,
668 follow_redirects=follow_redirects,
669 max_redirects=max_redirects,
670 event_hooks=event_hooks,
671 base_url=base_url,
672 trust_env=trust_env,
673 default_encoding=default_encoding,
674 )
676 if http2:
677 try:
678 import h2 # noqa
679 except ImportError: # pragma: no cover
680 raise ImportError(
681 "Using http2=True, but the 'h2' package is not installed. "
682 "Make sure to install httpx using `pip install httpx[http2]`."
683 ) from None
685 allow_env_proxies = trust_env and transport is None
686 proxy_map = self._get_proxy_map(proxy, allow_env_proxies)
688 self._transport = self._init_transport(
689 verify=verify,
690 cert=cert,
691 trust_env=trust_env,
692 http1=http1,
693 http2=http2,
694 limits=limits,
695 transport=transport,
696 )
697 self._mounts: dict[URLPattern, BaseTransport | None] = {
698 URLPattern(key): None
699 if proxy is None
700 else self._init_proxy_transport(
701 proxy,
702 verify=verify,
703 cert=cert,
704 trust_env=trust_env,
705 http1=http1,
706 http2=http2,
707 limits=limits,
708 )
709 for key, proxy in proxy_map.items()
710 }
711 if mounts is not None:
712 self._mounts.update(
713 {URLPattern(key): transport for key, transport in mounts.items()}
714 )
716 self._mounts = dict(sorted(self._mounts.items()))
718 def _init_transport(
719 self,
720 verify: ssl.SSLContext | str | bool = True,
721 cert: CertTypes | None = None,
722 trust_env: bool = True,
723 http1: bool = True,
724 http2: bool = False,
725 limits: Limits = DEFAULT_LIMITS,
726 transport: BaseTransport | None = None,
727 ) -> BaseTransport:
728 if transport is not None:
729 return transport
731 return HTTPTransport(
732 verify=verify,
733 cert=cert,
734 trust_env=trust_env,
735 http1=http1,
736 http2=http2,
737 limits=limits,
738 )
740 def _init_proxy_transport(
741 self,
742 proxy: Proxy,
743 verify: ssl.SSLContext | str | bool = True,
744 cert: CertTypes | None = None,
745 trust_env: bool = True,
746 http1: bool = True,
747 http2: bool = False,
748 limits: Limits = DEFAULT_LIMITS,
749 ) -> BaseTransport:
750 return HTTPTransport(
751 verify=verify,
752 cert=cert,
753 trust_env=trust_env,
754 http1=http1,
755 http2=http2,
756 limits=limits,
757 proxy=proxy,
758 )
760 def _transport_for_url(self, url: URL) -> BaseTransport:
761 """
762 Returns the transport instance that should be used for a given URL.
763 This will either be the standard connection pool, or a proxy.
764 """
765 for pattern, transport in self._mounts.items():
766 if pattern.matches(url):
767 return self._transport if transport is None else transport
769 return self._transport
771 def request(
772 self,
773 method: str,
774 url: URL | str,
775 *,
776 content: RequestContent | None = None,
777 data: RequestData | None = None,
778 files: RequestFiles | None = None,
779 json: typing.Any | None = None,
780 params: QueryParamTypes | None = None,
781 headers: HeaderTypes | None = None,
782 cookies: CookieTypes | None = None,
783 auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT,
784 follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
785 timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
786 extensions: RequestExtensions | None = None,
787 ) -> Response:
788 """
789 Build and send a request.
791 Equivalent to:
793 ```python
794 request = client.build_request(...)
795 response = client.send(request, ...)
796 ```
798 See `Client.build_request()`, `Client.send()` and
799 [Merging of configuration][0] for how the various parameters
800 are merged with client-level configuration.
802 [0]: /advanced/clients/#merging-of-configuration
803 """
804 if cookies is not None:
805 message = (
806 "Setting per-request cookies=<...> is being deprecated, because "
807 "the expected behaviour on cookie persistence is ambiguous. Set "
808 "cookies directly on the client instance instead."
809 )
810 warnings.warn(message, DeprecationWarning, stacklevel=2)
812 request = self.build_request(
813 method=method,
814 url=url,
815 content=content,
816 data=data,
817 files=files,
818 json=json,
819 params=params,
820 headers=headers,
821 cookies=cookies,
822 timeout=timeout,
823 extensions=extensions,
824 )
825 return self.send(request, auth=auth, follow_redirects=follow_redirects)
827 @contextmanager
828 def stream(
829 self,
830 method: str,
831 url: URL | str,
832 *,
833 content: RequestContent | None = None,
834 data: RequestData | None = None,
835 files: RequestFiles | None = None,
836 json: typing.Any | None = None,
837 params: QueryParamTypes | None = None,
838 headers: HeaderTypes | None = None,
839 cookies: CookieTypes | None = None,
840 auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT,
841 follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
842 timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
843 extensions: RequestExtensions | None = None,
844 ) -> typing.Iterator[Response]:
845 """
846 Alternative to `httpx.request()` that streams the response body
847 instead of loading it into memory at once.
849 **Parameters**: See `httpx.request`.
851 See also: [Streaming Responses][0]
853 [0]: /quickstart#streaming-responses
854 """
855 request = self.build_request(
856 method=method,
857 url=url,
858 content=content,
859 data=data,
860 files=files,
861 json=json,
862 params=params,
863 headers=headers,
864 cookies=cookies,
865 timeout=timeout,
866 extensions=extensions,
867 )
868 response = self.send(
869 request=request,
870 auth=auth,
871 follow_redirects=follow_redirects,
872 stream=True,
873 )
874 try:
875 yield response
876 finally:
877 response.close()
879 def send(
880 self,
881 request: Request,
882 *,
883 stream: bool = False,
884 auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT,
885 follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
886 ) -> Response:
887 """
888 Send a request.
890 The request is sent as-is, unmodified.
892 Typically you'll want to build one with `Client.build_request()`
893 so that any client-level configuration is merged into the request,
894 but passing an explicit `httpx.Request()` is supported as well.
896 See also: [Request instances][0]
898 [0]: /advanced/clients/#request-instances
899 """
900 if self._state == ClientState.CLOSED:
901 raise RuntimeError("Cannot send a request, as the client has been closed.")
903 self._state = ClientState.OPENED
904 follow_redirects = (
905 self.follow_redirects
906 if isinstance(follow_redirects, UseClientDefault)
907 else follow_redirects
908 )
910 self._set_timeout(request)
912 auth = self._build_request_auth(request, auth)
914 response = self._send_handling_auth(
915 request,
916 auth=auth,
917 follow_redirects=follow_redirects,
918 history=[],
919 )
920 try:
921 if not stream:
922 response.read()
924 return response
926 except BaseException as exc:
927 response.close()
928 raise exc
930 def _send_handling_auth(
931 self,
932 request: Request,
933 auth: Auth,
934 follow_redirects: bool,
935 history: list[Response],
936 ) -> Response:
937 auth_flow = auth.sync_auth_flow(request)
938 try:
939 request = next(auth_flow)
941 while True:
942 response = self._send_handling_redirects(
943 request,
944 follow_redirects=follow_redirects,
945 history=history,
946 )
947 try:
948 try:
949 next_request = auth_flow.send(response)
950 except StopIteration:
951 return response
953 response.history = list(history)
954 response.read()
955 request = next_request
956 history.append(response)
958 except BaseException as exc:
959 response.close()
960 raise exc
961 finally:
962 auth_flow.close()
964 def _send_handling_redirects(
965 self,
966 request: Request,
967 follow_redirects: bool,
968 history: list[Response],
969 ) -> Response:
970 while True:
971 if len(history) > self.max_redirects:
972 raise TooManyRedirects(
973 "Exceeded maximum allowed redirects.", request=request
974 )
976 for hook in self._event_hooks["request"]:
977 hook(request)
979 response = self._send_single_request(request)
980 try:
981 for hook in self._event_hooks["response"]:
982 hook(response)
983 response.history = list(history)
985 if not response.has_redirect_location:
986 return response
988 request = self._build_redirect_request(request, response)
989 history = history + [response]
991 if follow_redirects:
992 response.read()
993 else:
994 response.next_request = request
995 return response
997 except BaseException as exc:
998 response.close()
999 raise exc
1001 def _send_single_request(self, request: Request) -> Response:
1002 """
1003 Sends a single request, without handling any redirections.
1004 """
1005 transport = self._transport_for_url(request.url)
1006 start = time.perf_counter()
1008 if not isinstance(request.stream, SyncByteStream):
1009 raise RuntimeError(
1010 "Attempted to send an async request with a sync Client instance."
1011 )
1013 with request_context(request=request):
1014 response = transport.handle_request(request)
1016 assert isinstance(response.stream, SyncByteStream)
1018 response.request = request
1019 response.stream = BoundSyncStream(
1020 response.stream, response=response, start=start
1021 )
1022 self.cookies.extract_cookies(response)
1023 response.default_encoding = self._default_encoding
1025 logger.info(
1026 'HTTP Request: %s %s "%s %d %s"',
1027 request.method,
1028 request.url,
1029 response.http_version,
1030 response.status_code,
1031 response.reason_phrase,
1032 )
1034 return response
1036 def get(
1037 self,
1038 url: URL | str,
1039 *,
1040 params: QueryParamTypes | None = None,
1041 headers: HeaderTypes | None = None,
1042 cookies: CookieTypes | None = None,
1043 auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT,
1044 follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
1045 timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
1046 extensions: RequestExtensions | None = None,
1047 ) -> Response:
1048 """
1049 Send a `GET` request.
1051 **Parameters**: See `httpx.request`.
1052 """
1053 return self.request(
1054 "GET",
1055 url,
1056 params=params,
1057 headers=headers,
1058 cookies=cookies,
1059 auth=auth,
1060 follow_redirects=follow_redirects,
1061 timeout=timeout,
1062 extensions=extensions,
1063 )
1065 def options(
1066 self,
1067 url: URL | str,
1068 *,
1069 params: QueryParamTypes | None = None,
1070 headers: HeaderTypes | None = None,
1071 cookies: CookieTypes | None = None,
1072 auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
1073 follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
1074 timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
1075 extensions: RequestExtensions | None = None,
1076 ) -> Response:
1077 """
1078 Send an `OPTIONS` request.
1080 **Parameters**: See `httpx.request`.
1081 """
1082 return self.request(
1083 "OPTIONS",
1084 url,
1085 params=params,
1086 headers=headers,
1087 cookies=cookies,
1088 auth=auth,
1089 follow_redirects=follow_redirects,
1090 timeout=timeout,
1091 extensions=extensions,
1092 )
1094 def head(
1095 self,
1096 url: URL | str,
1097 *,
1098 params: QueryParamTypes | None = None,
1099 headers: HeaderTypes | None = None,
1100 cookies: CookieTypes | None = None,
1101 auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
1102 follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
1103 timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
1104 extensions: RequestExtensions | None = None,
1105 ) -> Response:
1106 """
1107 Send a `HEAD` request.
1109 **Parameters**: See `httpx.request`.
1110 """
1111 return self.request(
1112 "HEAD",
1113 url,
1114 params=params,
1115 headers=headers,
1116 cookies=cookies,
1117 auth=auth,
1118 follow_redirects=follow_redirects,
1119 timeout=timeout,
1120 extensions=extensions,
1121 )
1123 def post(
1124 self,
1125 url: URL | str,
1126 *,
1127 content: RequestContent | None = None,
1128 data: RequestData | None = None,
1129 files: RequestFiles | None = None,
1130 json: typing.Any | None = None,
1131 params: QueryParamTypes | None = None,
1132 headers: HeaderTypes | None = None,
1133 cookies: CookieTypes | None = None,
1134 auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
1135 follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
1136 timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
1137 extensions: RequestExtensions | None = None,
1138 ) -> Response:
1139 """
1140 Send a `POST` request.
1142 **Parameters**: See `httpx.request`.
1143 """
1144 return self.request(
1145 "POST",
1146 url,
1147 content=content,
1148 data=data,
1149 files=files,
1150 json=json,
1151 params=params,
1152 headers=headers,
1153 cookies=cookies,
1154 auth=auth,
1155 follow_redirects=follow_redirects,
1156 timeout=timeout,
1157 extensions=extensions,
1158 )
1160 def put(
1161 self,
1162 url: URL | str,
1163 *,
1164 content: RequestContent | None = None,
1165 data: RequestData | None = None,
1166 files: RequestFiles | None = None,
1167 json: typing.Any | None = None,
1168 params: QueryParamTypes | None = None,
1169 headers: HeaderTypes | None = None,
1170 cookies: CookieTypes | None = None,
1171 auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
1172 follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
1173 timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
1174 extensions: RequestExtensions | None = None,
1175 ) -> Response:
1176 """
1177 Send a `PUT` request.
1179 **Parameters**: See `httpx.request`.
1180 """
1181 return self.request(
1182 "PUT",
1183 url,
1184 content=content,
1185 data=data,
1186 files=files,
1187 json=json,
1188 params=params,
1189 headers=headers,
1190 cookies=cookies,
1191 auth=auth,
1192 follow_redirects=follow_redirects,
1193 timeout=timeout,
1194 extensions=extensions,
1195 )
1197 def patch(
1198 self,
1199 url: URL | str,
1200 *,
1201 content: RequestContent | None = None,
1202 data: RequestData | None = None,
1203 files: RequestFiles | None = None,
1204 json: typing.Any | None = None,
1205 params: QueryParamTypes | None = None,
1206 headers: HeaderTypes | None = None,
1207 cookies: CookieTypes | None = None,
1208 auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
1209 follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
1210 timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
1211 extensions: RequestExtensions | None = None,
1212 ) -> Response:
1213 """
1214 Send a `PATCH` request.
1216 **Parameters**: See `httpx.request`.
1217 """
1218 return self.request(
1219 "PATCH",
1220 url,
1221 content=content,
1222 data=data,
1223 files=files,
1224 json=json,
1225 params=params,
1226 headers=headers,
1227 cookies=cookies,
1228 auth=auth,
1229 follow_redirects=follow_redirects,
1230 timeout=timeout,
1231 extensions=extensions,
1232 )
1234 def delete(
1235 self,
1236 url: URL | str,
1237 *,
1238 params: QueryParamTypes | None = None,
1239 headers: HeaderTypes | None = None,
1240 cookies: CookieTypes | None = None,
1241 auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
1242 follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
1243 timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
1244 extensions: RequestExtensions | None = None,
1245 ) -> Response:
1246 """
1247 Send a `DELETE` request.
1249 **Parameters**: See `httpx.request`.
1250 """
1251 return self.request(
1252 "DELETE",
1253 url,
1254 params=params,
1255 headers=headers,
1256 cookies=cookies,
1257 auth=auth,
1258 follow_redirects=follow_redirects,
1259 timeout=timeout,
1260 extensions=extensions,
1261 )
1263 def close(self) -> None:
1264 """
1265 Close transport and proxies.
1266 """
1267 if self._state != ClientState.CLOSED:
1268 self._state = ClientState.CLOSED
1270 self._transport.close()
1271 for transport in self._mounts.values():
1272 if transport is not None:
1273 transport.close()
1275 def __enter__(self: T) -> T:
1276 if self._state != ClientState.UNOPENED:
1277 msg = {
1278 ClientState.OPENED: "Cannot open a client instance more than once.",
1279 ClientState.CLOSED: (
1280 "Cannot reopen a client instance, once it has been closed."
1281 ),
1282 }[self._state]
1283 raise RuntimeError(msg)
1285 self._state = ClientState.OPENED
1287 self._transport.__enter__()
1288 for transport in self._mounts.values():
1289 if transport is not None:
1290 transport.__enter__()
1291 return self
1293 def __exit__(
1294 self,
1295 exc_type: type[BaseException] | None = None,
1296 exc_value: BaseException | None = None,
1297 traceback: TracebackType | None = None,
1298 ) -> None:
1299 self._state = ClientState.CLOSED
1301 self._transport.__exit__(exc_type, exc_value, traceback)
1302 for transport in self._mounts.values():
1303 if transport is not None:
1304 transport.__exit__(exc_type, exc_value, traceback)
1307class AsyncClient(BaseClient):
1308 """
1309 An asynchronous HTTP client, with connection pooling, HTTP/2, redirects,
1310 cookie persistence, etc.
1312 It can be shared between tasks.
1314 Usage:
1316 ```python
1317 >>> async with httpx.AsyncClient() as client:
1318 >>> response = await client.get('https://example.org')
1319 ```
1321 **Parameters:**
1323 * **auth** - *(optional)* An authentication class to use when sending
1324 requests.
1325 * **params** - *(optional)* Query parameters to include in request URLs, as
1326 a string, dictionary, or sequence of two-tuples.
1327 * **headers** - *(optional)* Dictionary of HTTP headers to include when
1328 sending requests.
1329 * **cookies** - *(optional)* Dictionary of Cookie items to include when
1330 sending requests.
1331 * **verify** - *(optional)* Either `True` to use an SSL context with the
1332 default CA bundle, `False` to disable verification, or an instance of
1333 `ssl.SSLContext` to use a custom context.
1334 * **http2** - *(optional)* A boolean indicating if HTTP/2 support should be
1335 enabled. Defaults to `False`.
1336 * **proxy** - *(optional)* A proxy URL where all the traffic should be routed.
1337 * **timeout** - *(optional)* The timeout configuration to use when sending
1338 requests.
1339 * **limits** - *(optional)* The limits configuration to use.
1340 * **max_redirects** - *(optional)* The maximum number of redirect responses
1341 that should be followed.
1342 * **base_url** - *(optional)* A URL to use as the base when building
1343 request URLs.
1344 * **transport** - *(optional)* A transport class to use for sending requests
1345 over the network.
1346 * **trust_env** - *(optional)* Enables or disables usage of environment
1347 variables for configuration.
1348 * **default_encoding** - *(optional)* The default encoding to use for decoding
1349 response text, if no charset information is included in a response Content-Type
1350 header. Set to a callable for automatic character set detection. Default: "utf-8".
1351 """
1353 def __init__(
1354 self,
1355 *,
1356 auth: AuthTypes | None = None,
1357 params: QueryParamTypes | None = None,
1358 headers: HeaderTypes | None = None,
1359 cookies: CookieTypes | None = None,
1360 verify: ssl.SSLContext | str | bool = True,
1361 cert: CertTypes | None = None,
1362 http1: bool = True,
1363 http2: bool = False,
1364 proxy: ProxyTypes | None = None,
1365 mounts: None | (typing.Mapping[str, AsyncBaseTransport | None]) = None,
1366 timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
1367 follow_redirects: bool = False,
1368 limits: Limits = DEFAULT_LIMITS,
1369 max_redirects: int = DEFAULT_MAX_REDIRECTS,
1370 event_hooks: None | (typing.Mapping[str, list[EventHook]]) = None,
1371 base_url: URL | str = "",
1372 transport: AsyncBaseTransport | None = None,
1373 trust_env: bool = True,
1374 default_encoding: str | typing.Callable[[bytes], str] = "utf-8",
1375 ) -> None:
1376 super().__init__(
1377 auth=auth,
1378 params=params,
1379 headers=headers,
1380 cookies=cookies,
1381 timeout=timeout,
1382 follow_redirects=follow_redirects,
1383 max_redirects=max_redirects,
1384 event_hooks=event_hooks,
1385 base_url=base_url,
1386 trust_env=trust_env,
1387 default_encoding=default_encoding,
1388 )
1390 if http2:
1391 try:
1392 import h2 # noqa
1393 except ImportError: # pragma: no cover
1394 raise ImportError(
1395 "Using http2=True, but the 'h2' package is not installed. "
1396 "Make sure to install httpx using `pip install httpx[http2]`."
1397 ) from None
1399 allow_env_proxies = trust_env and transport is None
1400 proxy_map = self._get_proxy_map(proxy, allow_env_proxies)
1402 self._transport = self._init_transport(
1403 verify=verify,
1404 cert=cert,
1405 trust_env=trust_env,
1406 http1=http1,
1407 http2=http2,
1408 limits=limits,
1409 transport=transport,
1410 )
1412 self._mounts: dict[URLPattern, AsyncBaseTransport | None] = {
1413 URLPattern(key): None
1414 if proxy is None
1415 else self._init_proxy_transport(
1416 proxy,
1417 verify=verify,
1418 cert=cert,
1419 trust_env=trust_env,
1420 http1=http1,
1421 http2=http2,
1422 limits=limits,
1423 )
1424 for key, proxy in proxy_map.items()
1425 }
1426 if mounts is not None:
1427 self._mounts.update(
1428 {URLPattern(key): transport for key, transport in mounts.items()}
1429 )
1430 self._mounts = dict(sorted(self._mounts.items()))
1432 def _init_transport(
1433 self,
1434 verify: ssl.SSLContext | str | bool = True,
1435 cert: CertTypes | None = None,
1436 trust_env: bool = True,
1437 http1: bool = True,
1438 http2: bool = False,
1439 limits: Limits = DEFAULT_LIMITS,
1440 transport: AsyncBaseTransport | None = None,
1441 ) -> AsyncBaseTransport:
1442 if transport is not None:
1443 return transport
1445 return AsyncHTTPTransport(
1446 verify=verify,
1447 cert=cert,
1448 trust_env=trust_env,
1449 http1=http1,
1450 http2=http2,
1451 limits=limits,
1452 )
1454 def _init_proxy_transport(
1455 self,
1456 proxy: Proxy,
1457 verify: ssl.SSLContext | str | bool = True,
1458 cert: CertTypes | None = None,
1459 trust_env: bool = True,
1460 http1: bool = True,
1461 http2: bool = False,
1462 limits: Limits = DEFAULT_LIMITS,
1463 ) -> AsyncBaseTransport:
1464 return AsyncHTTPTransport(
1465 verify=verify,
1466 cert=cert,
1467 trust_env=trust_env,
1468 http1=http1,
1469 http2=http2,
1470 limits=limits,
1471 proxy=proxy,
1472 )
1474 def _transport_for_url(self, url: URL) -> AsyncBaseTransport:
1475 """
1476 Returns the transport instance that should be used for a given URL.
1477 This will either be the standard connection pool, or a proxy.
1478 """
1479 for pattern, transport in self._mounts.items():
1480 if pattern.matches(url):
1481 return self._transport if transport is None else transport
1483 return self._transport
1485 async def request(
1486 self,
1487 method: str,
1488 url: URL | str,
1489 *,
1490 content: RequestContent | None = None,
1491 data: RequestData | None = None,
1492 files: RequestFiles | None = None,
1493 json: typing.Any | None = None,
1494 params: QueryParamTypes | None = None,
1495 headers: HeaderTypes | None = None,
1496 cookies: CookieTypes | None = None,
1497 auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT,
1498 follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
1499 timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
1500 extensions: RequestExtensions | None = None,
1501 ) -> Response:
1502 """
1503 Build and send a request.
1505 Equivalent to:
1507 ```python
1508 request = client.build_request(...)
1509 response = await client.send(request, ...)
1510 ```
1512 See `AsyncClient.build_request()`, `AsyncClient.send()`
1513 and [Merging of configuration][0] for how the various parameters
1514 are merged with client-level configuration.
1516 [0]: /advanced/clients/#merging-of-configuration
1517 """
1519 if cookies is not None: # pragma: no cover
1520 message = (
1521 "Setting per-request cookies=<...> is being deprecated, because "
1522 "the expected behaviour on cookie persistence is ambiguous. Set "
1523 "cookies directly on the client instance instead."
1524 )
1525 warnings.warn(message, DeprecationWarning, stacklevel=2)
1527 request = self.build_request(
1528 method=method,
1529 url=url,
1530 content=content,
1531 data=data,
1532 files=files,
1533 json=json,
1534 params=params,
1535 headers=headers,
1536 cookies=cookies,
1537 timeout=timeout,
1538 extensions=extensions,
1539 )
1540 return await self.send(request, auth=auth, follow_redirects=follow_redirects)
1542 @asynccontextmanager
1543 async def stream(
1544 self,
1545 method: str,
1546 url: URL | str,
1547 *,
1548 content: RequestContent | None = None,
1549 data: RequestData | None = None,
1550 files: RequestFiles | None = None,
1551 json: typing.Any | None = None,
1552 params: QueryParamTypes | None = None,
1553 headers: HeaderTypes | None = None,
1554 cookies: CookieTypes | None = None,
1555 auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT,
1556 follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
1557 timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
1558 extensions: RequestExtensions | None = None,
1559 ) -> typing.AsyncIterator[Response]:
1560 """
1561 Alternative to `httpx.request()` that streams the response body
1562 instead of loading it into memory at once.
1564 **Parameters**: See `httpx.request`.
1566 See also: [Streaming Responses][0]
1568 [0]: /quickstart#streaming-responses
1569 """
1570 request = self.build_request(
1571 method=method,
1572 url=url,
1573 content=content,
1574 data=data,
1575 files=files,
1576 json=json,
1577 params=params,
1578 headers=headers,
1579 cookies=cookies,
1580 timeout=timeout,
1581 extensions=extensions,
1582 )
1583 response = await self.send(
1584 request=request,
1585 auth=auth,
1586 follow_redirects=follow_redirects,
1587 stream=True,
1588 )
1589 try:
1590 yield response
1591 finally:
1592 await response.aclose()
1594 async def send(
1595 self,
1596 request: Request,
1597 *,
1598 stream: bool = False,
1599 auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT,
1600 follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
1601 ) -> Response:
1602 """
1603 Send a request.
1605 The request is sent as-is, unmodified.
1607 Typically you'll want to build one with `AsyncClient.build_request()`
1608 so that any client-level configuration is merged into the request,
1609 but passing an explicit `httpx.Request()` is supported as well.
1611 See also: [Request instances][0]
1613 [0]: /advanced/clients/#request-instances
1614 """
1615 if self._state == ClientState.CLOSED:
1616 raise RuntimeError("Cannot send a request, as the client has been closed.")
1618 self._state = ClientState.OPENED
1619 follow_redirects = (
1620 self.follow_redirects
1621 if isinstance(follow_redirects, UseClientDefault)
1622 else follow_redirects
1623 )
1625 self._set_timeout(request)
1627 auth = self._build_request_auth(request, auth)
1629 response = await self._send_handling_auth(
1630 request,
1631 auth=auth,
1632 follow_redirects=follow_redirects,
1633 history=[],
1634 )
1635 try:
1636 if not stream:
1637 await response.aread()
1639 return response
1641 except BaseException as exc:
1642 await response.aclose()
1643 raise exc
1645 async def _send_handling_auth(
1646 self,
1647 request: Request,
1648 auth: Auth,
1649 follow_redirects: bool,
1650 history: list[Response],
1651 ) -> Response:
1652 auth_flow = auth.async_auth_flow(request)
1653 try:
1654 request = await auth_flow.__anext__()
1656 while True:
1657 response = await self._send_handling_redirects(
1658 request,
1659 follow_redirects=follow_redirects,
1660 history=history,
1661 )
1662 try:
1663 try:
1664 next_request = await auth_flow.asend(response)
1665 except StopAsyncIteration:
1666 return response
1668 response.history = list(history)
1669 await response.aread()
1670 request = next_request
1671 history.append(response)
1673 except BaseException as exc:
1674 await response.aclose()
1675 raise exc
1676 finally:
1677 await auth_flow.aclose()
1679 async def _send_handling_redirects(
1680 self,
1681 request: Request,
1682 follow_redirects: bool,
1683 history: list[Response],
1684 ) -> Response:
1685 while True:
1686 if len(history) > self.max_redirects:
1687 raise TooManyRedirects(
1688 "Exceeded maximum allowed redirects.", request=request
1689 )
1691 for hook in self._event_hooks["request"]:
1692 await hook(request)
1694 response = await self._send_single_request(request)
1695 try:
1696 for hook in self._event_hooks["response"]:
1697 await hook(response)
1699 response.history = list(history)
1701 if not response.has_redirect_location:
1702 return response
1704 request = self._build_redirect_request(request, response)
1705 history = history + [response]
1707 if follow_redirects:
1708 await response.aread()
1709 else:
1710 response.next_request = request
1711 return response
1713 except BaseException as exc:
1714 await response.aclose()
1715 raise exc
1717 async def _send_single_request(self, request: Request) -> Response:
1718 """
1719 Sends a single request, without handling any redirections.
1720 """
1721 transport = self._transport_for_url(request.url)
1722 start = time.perf_counter()
1724 if not isinstance(request.stream, AsyncByteStream):
1725 raise RuntimeError(
1726 "Attempted to send an sync request with an AsyncClient instance."
1727 )
1729 with request_context(request=request):
1730 response = await transport.handle_async_request(request)
1732 assert isinstance(response.stream, AsyncByteStream)
1733 response.request = request
1734 response.stream = BoundAsyncStream(
1735 response.stream, response=response, start=start
1736 )
1737 self.cookies.extract_cookies(response)
1738 response.default_encoding = self._default_encoding
1740 logger.info(
1741 'HTTP Request: %s %s "%s %d %s"',
1742 request.method,
1743 request.url,
1744 response.http_version,
1745 response.status_code,
1746 response.reason_phrase,
1747 )
1749 return response
1751 async def get(
1752 self,
1753 url: URL | str,
1754 *,
1755 params: QueryParamTypes | None = None,
1756 headers: HeaderTypes | None = None,
1757 cookies: CookieTypes | None = None,
1758 auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT,
1759 follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
1760 timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
1761 extensions: RequestExtensions | None = None,
1762 ) -> Response:
1763 """
1764 Send a `GET` request.
1766 **Parameters**: See `httpx.request`.
1767 """
1768 return await self.request(
1769 "GET",
1770 url,
1771 params=params,
1772 headers=headers,
1773 cookies=cookies,
1774 auth=auth,
1775 follow_redirects=follow_redirects,
1776 timeout=timeout,
1777 extensions=extensions,
1778 )
1780 async def options(
1781 self,
1782 url: URL | str,
1783 *,
1784 params: QueryParamTypes | None = None,
1785 headers: HeaderTypes | None = None,
1786 cookies: CookieTypes | None = None,
1787 auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
1788 follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
1789 timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
1790 extensions: RequestExtensions | None = None,
1791 ) -> Response:
1792 """
1793 Send an `OPTIONS` request.
1795 **Parameters**: See `httpx.request`.
1796 """
1797 return await self.request(
1798 "OPTIONS",
1799 url,
1800 params=params,
1801 headers=headers,
1802 cookies=cookies,
1803 auth=auth,
1804 follow_redirects=follow_redirects,
1805 timeout=timeout,
1806 extensions=extensions,
1807 )
1809 async def head(
1810 self,
1811 url: URL | str,
1812 *,
1813 params: QueryParamTypes | None = None,
1814 headers: HeaderTypes | None = None,
1815 cookies: CookieTypes | None = None,
1816 auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
1817 follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
1818 timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
1819 extensions: RequestExtensions | None = None,
1820 ) -> Response:
1821 """
1822 Send a `HEAD` request.
1824 **Parameters**: See `httpx.request`.
1825 """
1826 return await self.request(
1827 "HEAD",
1828 url,
1829 params=params,
1830 headers=headers,
1831 cookies=cookies,
1832 auth=auth,
1833 follow_redirects=follow_redirects,
1834 timeout=timeout,
1835 extensions=extensions,
1836 )
1838 async def post(
1839 self,
1840 url: URL | str,
1841 *,
1842 content: RequestContent | None = None,
1843 data: RequestData | None = None,
1844 files: RequestFiles | None = None,
1845 json: typing.Any | None = None,
1846 params: QueryParamTypes | None = None,
1847 headers: HeaderTypes | None = None,
1848 cookies: CookieTypes | None = None,
1849 auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
1850 follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
1851 timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
1852 extensions: RequestExtensions | None = None,
1853 ) -> Response:
1854 """
1855 Send a `POST` request.
1857 **Parameters**: See `httpx.request`.
1858 """
1859 return await self.request(
1860 "POST",
1861 url,
1862 content=content,
1863 data=data,
1864 files=files,
1865 json=json,
1866 params=params,
1867 headers=headers,
1868 cookies=cookies,
1869 auth=auth,
1870 follow_redirects=follow_redirects,
1871 timeout=timeout,
1872 extensions=extensions,
1873 )
1875 async def put(
1876 self,
1877 url: URL | str,
1878 *,
1879 content: RequestContent | None = None,
1880 data: RequestData | None = None,
1881 files: RequestFiles | None = None,
1882 json: typing.Any | None = None,
1883 params: QueryParamTypes | None = None,
1884 headers: HeaderTypes | None = None,
1885 cookies: CookieTypes | None = None,
1886 auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
1887 follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
1888 timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
1889 extensions: RequestExtensions | None = None,
1890 ) -> Response:
1891 """
1892 Send a `PUT` request.
1894 **Parameters**: See `httpx.request`.
1895 """
1896 return await self.request(
1897 "PUT",
1898 url,
1899 content=content,
1900 data=data,
1901 files=files,
1902 json=json,
1903 params=params,
1904 headers=headers,
1905 cookies=cookies,
1906 auth=auth,
1907 follow_redirects=follow_redirects,
1908 timeout=timeout,
1909 extensions=extensions,
1910 )
1912 async def patch(
1913 self,
1914 url: URL | str,
1915 *,
1916 content: RequestContent | None = None,
1917 data: RequestData | None = None,
1918 files: RequestFiles | None = None,
1919 json: typing.Any | None = None,
1920 params: QueryParamTypes | None = None,
1921 headers: HeaderTypes | None = None,
1922 cookies: CookieTypes | None = None,
1923 auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
1924 follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
1925 timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
1926 extensions: RequestExtensions | None = None,
1927 ) -> Response:
1928 """
1929 Send a `PATCH` request.
1931 **Parameters**: See `httpx.request`.
1932 """
1933 return await self.request(
1934 "PATCH",
1935 url,
1936 content=content,
1937 data=data,
1938 files=files,
1939 json=json,
1940 params=params,
1941 headers=headers,
1942 cookies=cookies,
1943 auth=auth,
1944 follow_redirects=follow_redirects,
1945 timeout=timeout,
1946 extensions=extensions,
1947 )
1949 async def delete(
1950 self,
1951 url: URL | str,
1952 *,
1953 params: QueryParamTypes | None = None,
1954 headers: HeaderTypes | None = None,
1955 cookies: CookieTypes | None = None,
1956 auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
1957 follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
1958 timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
1959 extensions: RequestExtensions | None = None,
1960 ) -> Response:
1961 """
1962 Send a `DELETE` request.
1964 **Parameters**: See `httpx.request`.
1965 """
1966 return await self.request(
1967 "DELETE",
1968 url,
1969 params=params,
1970 headers=headers,
1971 cookies=cookies,
1972 auth=auth,
1973 follow_redirects=follow_redirects,
1974 timeout=timeout,
1975 extensions=extensions,
1976 )
1978 async def aclose(self) -> None:
1979 """
1980 Close transport and proxies.
1981 """
1982 if self._state != ClientState.CLOSED:
1983 self._state = ClientState.CLOSED
1985 await self._transport.aclose()
1986 for proxy in self._mounts.values():
1987 if proxy is not None:
1988 await proxy.aclose()
1990 async def __aenter__(self: U) -> U:
1991 if self._state != ClientState.UNOPENED:
1992 msg = {
1993 ClientState.OPENED: "Cannot open a client instance more than once.",
1994 ClientState.CLOSED: (
1995 "Cannot reopen a client instance, once it has been closed."
1996 ),
1997 }[self._state]
1998 raise RuntimeError(msg)
2000 self._state = ClientState.OPENED
2002 await self._transport.__aenter__()
2003 for proxy in self._mounts.values():
2004 if proxy is not None:
2005 await proxy.__aenter__()
2006 return self
2008 async def __aexit__(
2009 self,
2010 exc_type: type[BaseException] | None = None,
2011 exc_value: BaseException | None = None,
2012 traceback: TracebackType | None = None,
2013 ) -> None:
2014 self._state = ClientState.CLOSED
2016 await self._transport.__aexit__(exc_type, exc_value, traceback)
2017 for proxy in self._mounts.values():
2018 if proxy is not None:
2019 await proxy.__aexit__(exc_type, exc_value, traceback)