← requests / src/requests/models.py
| 1 | """ |
| 2 | requests.models |
| 3 | ~~~~~~~~~~~~~~~ |
| 4 | |
| 5 | This module contains the primary objects that power Requests. |
| 6 | """ |
| 7 | |
| 8 | from __future__ import annotations |
| 9 | |
| 10 | import datetime |
| 11 | |
| 12 | # Import encoding now, to avoid implicit import later. |
| 13 | # Implicit import within threads may cause LookupError when standard library is in a ZIP, |
| 14 | # such as in Embedded Python. See https://github.com/psf/requests/issues/3578. |
| 15 | import encodings.idna # noqa: F401 # type: ignore[reportUnusedImport] |
| 16 | from collections.abc import Callable, Generator, Iterable, Iterator, Mapping |
| 17 | from io import UnsupportedOperation |
| 18 | from typing import ( |
| 19 | TYPE_CHECKING, |
| 20 | Any, |
| 21 | Final, |
| 22 | Literal, |
| 23 | cast, |
| 24 | overload, |
| 25 | ) |
| 26 | |
| 27 | from urllib3.exceptions import ( |
| 28 | DecodeError, |
| 29 | LocationParseError, |
| 30 | ProtocolError, |
| 31 | ReadTimeoutError, |
| 32 | SSLError, |
| 33 | ) |
| 34 | from urllib3.fields import RequestField |
| 35 | from urllib3.filepost import encode_multipart_formdata |
| 36 | from urllib3.util import parse_url |
| 37 | |
| 38 | from ._internal_utils import to_native_string, unicode_is_ascii |
| 39 | from ._types import SupportsRead as _SupportsRead |
| 40 | from .auth import HTTPBasicAuth |
| 41 | from .compat import ( |
| 42 | JSONDecodeError, |
| 43 | basestring, |
| 44 | builtin_str, |
| 45 | chardet, |
| 46 | cookielib, |
| 47 | urlencode, |
| 48 | urlsplit, |
| 49 | urlunparse, |
| 50 | ) |
| 51 | from .compat import json as complexjson |
| 52 | from .cookies import ( |
| 53 | _copy_cookie_jar, # type: ignore[reportPrivateUsage] |
| 54 | cookiejar_from_dict, |
| 55 | get_cookie_header, |
| 56 | ) |
| 57 | from .exceptions import ( |
| 58 | ChunkedEncodingError, |
| 59 | ConnectionError, |
| 60 | ContentDecodingError, |
| 61 | HTTPError, |
| 62 | InvalidJSONError, |
| 63 | InvalidURL, |
| 64 | MissingSchema, |
| 65 | StreamConsumedError, |
| 66 | ) |
| 67 | from .exceptions import JSONDecodeError as RequestsJSONDecodeError |
| 68 | from .exceptions import SSLError as RequestsSSLError |
| 69 | from .hooks import default_hooks |
| 70 | from .status_codes import codes |
| 71 | from .structures import CaseInsensitiveDict |
| 72 | from .utils import ( |
| 73 | check_header_validity, |
| 74 | get_auth_from_url, |
| 75 | guess_filename, |
| 76 | guess_json_utf, |
| 77 | iter_slices, |
| 78 | parse_header_links, |
| 79 | requote_uri, |
| 80 | stream_decode_response_unicode, |
| 81 | super_len, |
| 82 | to_key_val_list, |
| 83 | ) |
| 84 | |
| 85 | if TYPE_CHECKING: |
| 86 | from http.cookiejar import CookieJar |
| 87 | |
| 88 | from typing_extensions import Self |
| 89 | |
| 90 | from . import _types as _t |
| 91 | from .adapters import HTTPAdapter |
| 92 | from .cookies import RequestsCookieJar |
| 93 | |
| 94 | #: The set of HTTP status codes that indicate an automatically |
| 95 | #: processable redirect. |
| 96 | REDIRECT_STATI: Final[tuple[int, ...]] = ( # type: ignore[assignment] |
| 97 | codes.moved, # 301 |
| 98 | codes.found, # 302 |
| 99 | codes.other, # 303 |
| 100 | codes.temporary_redirect, # 307 |
| 101 | codes.permanent_redirect, # 308 |
| 102 | ) |
| 103 | |
| 104 | DEFAULT_REDIRECT_LIMIT: int = 30 |
| 105 | CONTENT_CHUNK_SIZE: int = 10 * 1024 |
| 106 | ITER_CHUNK_SIZE: int = 512 |
| 107 | |
| 108 | |
| 109 | class RequestEncodingMixin: |
| 110 | url: str | None |
| 111 | |
| 112 | @property |
| 113 | def path_url(self) -> str: |
| 114 | """Build the path URL to use.""" |
| 115 | |
| 116 | url: list[str] = [] |
| 117 | |
| 118 | p = urlsplit(cast(str, self.url)) |
| 119 | |
| 120 | path = p.path |
| 121 | if not path: |
| 122 | path = "/" |
| 123 | |
| 124 | url.append(path) |
| 125 | |
| 126 | query = p.query |
| 127 | if query: |
| 128 | url.append("?") |
| 129 | url.append(query) |
| 130 | |
| 131 | return "".join(url) |
| 132 | |
| 133 | @overload |
| 134 | @staticmethod |
| 135 | def _encode_params(data: str) -> str: ... |
| 136 | |
| 137 | @overload |
| 138 | @staticmethod |
| 139 | def _encode_params(data: bytes) -> bytes: ... |
| 140 | |
| 141 | @overload |
| 142 | @staticmethod |
| 143 | def _encode_params( |
| 144 | data: _t.SupportsRead[str | bytes], |
| 145 | ) -> _t.SupportsRead[str | bytes]: ... |
| 146 | |
| 147 | @overload |
| 148 | @staticmethod |
| 149 | def _encode_params(data: _t.KVDataType) -> str: ... |
| 150 | |
| 151 | @staticmethod |
| 152 | def _encode_params( |
| 153 | data: _t.EncodableDataType, |
| 154 | ) -> str | bytes | _t.SupportsRead[str | bytes]: |
| 155 | """Encode parameters in a piece of data. |
| 156 | |
| 157 | Will successfully encode parameters when passed as a dict or a list of |
| 158 | 2-tuples. Order is retained if data is a list of 2-tuples but arbitrary |
| 159 | if parameters are supplied as a dict. |
| 160 | """ |
| 161 | |
| 162 | if isinstance(data, (str, bytes)): |
| 163 | return data |
| 164 | elif isinstance(data, _SupportsRead): |
| 165 | return data |
| 166 | elif hasattr(data, "__iter__"): |
| 167 | result: list[tuple[bytes, bytes]] = [] |
| 168 | for k, vs in to_key_val_list(data): |
| 169 | if isinstance(vs, basestring) or not hasattr(vs, "__iter__"): |
| 170 | vs = [vs] |
| 171 | for v in vs: |
| 172 | if v is not None: |
| 173 | result.append( |
| 174 | ( |
| 175 | k.encode("utf-8") if isinstance(k, str) else k, |
| 176 | v.encode("utf-8") if isinstance(v, str) else v, |
| 177 | ) |
| 178 | ) |
| 179 | return urlencode(result, doseq=True) |
| 180 | else: |
| 181 | return data # type: ignore[return-value] # unreachable for valid _t.DataType |
| 182 | |
| 183 | @staticmethod |
| 184 | def _encode_files( |
| 185 | files: _t.FilesType, data: _t.RawDataType | None |
| 186 | ) -> tuple[bytes, str]: |
| 187 | """Build the body for a multipart/form-data request. |
| 188 | |
| 189 | Will successfully encode files when passed as a dict or a list of |
| 190 | tuples. Order is retained if data is a list of tuples but arbitrary |
| 191 | if parameters are supplied as a dict. |
| 192 | The tuples may be 2-tuples (filename, fileobj), 3-tuples (filename, fileobj, contentype) |
| 193 | or 4-tuples (filename, fileobj, contentype, custom_headers). |
| 194 | """ |
| 195 | if not files: |
| 196 | raise ValueError("Files must be provided.") |
| 197 | elif isinstance(data, basestring): |
| 198 | raise ValueError("Data must not be a string.") |
| 199 | |
| 200 | new_fields: list[RequestField | tuple[str, bytes]] = [] |
| 201 | fields = to_key_val_list(data or {}) |
| 202 | files = to_key_val_list(files or {}) |
| 203 | |
| 204 | for field, val in fields: |
| 205 | if isinstance(val, basestring) or not hasattr(val, "__iter__"): |
| 206 | val = [val] |
| 207 | for v in val: |
| 208 | if v is not None: |
| 209 | # Don't call str() on bytestrings: in Py3 it all goes wrong. |
| 210 | if not isinstance(v, bytes): |
| 211 | v = str(v) |
| 212 | |
| 213 | new_fields.append( |
| 214 | ( |
| 215 | field.decode("utf-8") |
| 216 | if isinstance(field, bytes) |
| 217 | else field, |
| 218 | v.encode("utf-8") if isinstance(v, str) else v, |
| 219 | ) |
| 220 | ) |
| 221 | |
| 222 | for k, v in files: |
| 223 | # support for explicit filename |
| 224 | ft = None |
| 225 | fh = None |
| 226 | if isinstance(v, (tuple, list)): |
| 227 | if len(v) == 2: |
| 228 | fn, fp = v |
| 229 | elif len(v) == 3: |
| 230 | fn, fp, ft = v |
| 231 | else: |
| 232 | fn, fp, ft, fh = v |
| 233 | else: |
| 234 | fn = guess_filename(v) or k |
| 235 | fp = v |
| 236 | |
| 237 | if isinstance(fp, (str, bytes, bytearray)): |
| 238 | fdata = fp |
| 239 | elif isinstance(fp, _SupportsRead): # type: ignore[reportUnnecessaryIsInstance] # defensive check for untyped callers |
| 240 | fdata = fp.read() |
| 241 | elif fp is None: # type: ignore[reportUnnecessaryComparison] # defensive check for untyped callers |
| 242 | continue |
| 243 | else: |
| 244 | fdata = fp |
| 245 | |
| 246 | rf = RequestField(name=k, data=fdata, filename=fn, headers=fh) |
| 247 | rf.make_multipart(content_type=ft) |
| 248 | new_fields.append(rf) |
| 249 | |
| 250 | body, content_type = encode_multipart_formdata(new_fields) |
| 251 | |
| 252 | return body, content_type |
| 253 | |
| 254 | |
| 255 | class RequestHooksMixin: |
| 256 | hooks: dict[str, list[_t.HookType]] |
| 257 | |
| 258 | def register_hook( |
| 259 | self, event: str, hook: Iterable[_t.HookType] | _t.HookType |
| 260 | ) -> None: |
| 261 | """Properly register a hook.""" |
| 262 | |
| 263 | if event not in self.hooks: |
| 264 | raise ValueError(f'Unsupported event specified, with event name "{event}"') |
| 265 | |
| 266 | if isinstance(hook, Callable): |
| 267 | self.hooks[event].append(hook) |
| 268 | elif hasattr(hook, "__iter__"): |
| 269 | self.hooks[event].extend(h for h in hook if isinstance(h, Callable)) # type: ignore[reportUnnecessaryIsInstance] # defensive runtime filter |
| 270 | |
| 271 | def deregister_hook(self, event: str, hook: _t.HookType) -> bool: |
| 272 | """Deregister a previously registered hook. |
| 273 | Returns True if the hook existed, False if not. |
| 274 | """ |
| 275 | |
| 276 | try: |
| 277 | self.hooks[event].remove(hook) |
| 278 | return True |
| 279 | except ValueError: |
| 280 | return False |
| 281 | |
| 282 | |
| 283 | class Request(RequestHooksMixin): |
| 284 | """A user-created :class:`Request <Request>` object. |
| 285 | |
| 286 | Used to prepare a :class:`PreparedRequest <PreparedRequest>`, which is sent to the server. |
| 287 | |
| 288 | :param method: HTTP method to use. |
| 289 | :param url: URL to send. |
| 290 | :param headers: dictionary of headers to send. |
| 291 | :param files: dictionary of {filename: fileobject} files to multipart upload. |
| 292 | :param data: the body to attach to the request. If a dictionary or |
| 293 | list of tuples ``[(key, value)]`` is provided, form-encoding will |
| 294 | take place. |
| 295 | :param json: json for the body to attach to the request (if files or data is not specified). |
| 296 | :param params: URL parameters to append to the URL. If a dictionary or |
| 297 | list of tuples ``[(key, value)]`` is provided, form-encoding will |
| 298 | take place. |
| 299 | :param auth: Auth handler or (user, pass) tuple. |
| 300 | :param cookies: dictionary or CookieJar of cookies to attach to this request. |
| 301 | :param hooks: dictionary of callback hooks, for internal usage. |
| 302 | |
| 303 | Usage:: |
| 304 | |
| 305 | >>> import requests |
| 306 | >>> req = requests.Request('GET', 'https://httpbin.org/get') |
| 307 | >>> req.prepare() |
| 308 | <PreparedRequest [GET]> |
| 309 | """ |
| 310 | |
| 311 | method: str | None |
| 312 | url: _t.UriType | None |
| 313 | headers: Mapping[str, str | bytes] |
| 314 | files: _t.FilesType |
| 315 | data: _t.DataType |
| 316 | json: _t.JsonType |
| 317 | params: _t.ParamsType |
| 318 | auth: _t.AuthType |
| 319 | cookies: RequestsCookieJar | CookieJar | dict[str, str] | None |
| 320 | |
| 321 | def __init__( |
| 322 | self, |
| 323 | method: str | None = None, |
| 324 | url: _t.UriType | None = None, |
| 325 | headers: _t.HeadersType = None, |
| 326 | files: _t.FilesType = None, |
| 327 | data: _t.DataType = None, |
| 328 | params: _t.ParamsType = None, |
| 329 | auth: _t.AuthType = None, |
| 330 | cookies: RequestsCookieJar | CookieJar | dict[str, str] | None = None, |
| 331 | hooks: _t.HooksInputType | None = None, |
| 332 | json: _t.JsonType = None, |
| 333 | ) -> None: |
| 334 | # Default empty dicts for dict params. |
| 335 | data = [] if data is None else data |
| 336 | files = [] if files is None else files |
| 337 | headers = {} if headers is None else headers |
| 338 | params = {} if params is None else params |
| 339 | hooks = {} if hooks is None else hooks |
| 340 | |
| 341 | self.hooks = default_hooks() |
| 342 | for k, v in list(hooks.items()): |
| 343 | self.register_hook(event=k, hook=v) |
| 344 | |
| 345 | self.method = method |
| 346 | self.url = url |
| 347 | self.headers = headers |
| 348 | self.files = files |
| 349 | self.data = data |
| 350 | self.json = json |
| 351 | self.params = params |
| 352 | self.auth = auth |
| 353 | self.cookies = cookies |
| 354 | |
| 355 | def __repr__(self) -> str: |
| 356 | return f"<Request [{self.method}]>" |
| 357 | |
| 358 | def prepare(self) -> PreparedRequest: |
| 359 | """Constructs a :class:`PreparedRequest <PreparedRequest>` for transmission and returns it.""" |
| 360 | p = PreparedRequest() |
| 361 | p.prepare( |
| 362 | method=self.method, |
| 363 | url=self.url, |
| 364 | headers=self.headers, |
| 365 | files=self.files, |
| 366 | data=self.data, |
| 367 | json=self.json, |
| 368 | params=self.params, |
| 369 | auth=self.auth, |
| 370 | cookies=self.cookies, |
| 371 | hooks=self.hooks, |
| 372 | ) |
| 373 | return p |
| 374 | |
| 375 | |
| 376 | class PreparedRequest(RequestEncodingMixin, RequestHooksMixin): |
| 377 | """The fully mutable :class:`PreparedRequest <PreparedRequest>` object, |
| 378 | containing the exact bytes that will be sent to the server. |
| 379 | |
| 380 | Instances are generated from a :class:`Request <Request>` object, and |
| 381 | should not be instantiated manually; doing so may produce undesirable |
| 382 | effects. |
| 383 | |
| 384 | Usage:: |
| 385 | |
| 386 | >>> import requests |
| 387 | >>> req = requests.Request('GET', 'https://httpbin.org/get') |
| 388 | >>> r = req.prepare() |
| 389 | >>> r |
| 390 | <PreparedRequest [GET]> |
| 391 | |
| 392 | >>> s = requests.Session() |
| 393 | >>> s.send(r) |
| 394 | <Response [200]> |
| 395 | """ |
| 396 | |
| 397 | method: str | None |
| 398 | url: str | None |
| 399 | headers: CaseInsensitiveDict[str | bytes] |
| 400 | _cookies: RequestsCookieJar | CookieJar | None |
| 401 | body: _t.BodyType |
| 402 | hooks: dict[str, list[_t.HookType]] |
| 403 | _body_position: int | object | None |
| 404 | |
| 405 | def __init__(self) -> None: |
| 406 | #: HTTP verb to send to the server. |
| 407 | self.method = None |
| 408 | #: HTTP URL to send the request to. |
| 409 | self.url = None |
| 410 | #: dictionary of HTTP headers. |
| 411 | self.headers = None # type: ignore[assignment] |
| 412 | # The `CookieJar` used to create the Cookie header will be stored here |
| 413 | # after prepare_cookies is called |
| 414 | self._cookies = None |
| 415 | #: request body to send to the server. |
| 416 | self.body = None |
| 417 | #: dictionary of callback hooks, for internal usage. |
| 418 | self.hooks = default_hooks() |
| 419 | #: integer denoting starting position of a readable file-like body. |
| 420 | self._body_position = None |
| 421 | |
| 422 | def prepare( |
| 423 | self, |
| 424 | method: str | None = None, |
| 425 | url: _t.UriType | None = None, |
| 426 | headers: Mapping[str, str | bytes] | None = None, |
| 427 | files: _t.FilesType = None, |
| 428 | data: _t.DataType = None, |
| 429 | params: _t.ParamsType = None, |
| 430 | auth: _t.AuthType = None, |
| 431 | cookies: RequestsCookieJar | CookieJar | dict[str, str] | None = None, |
| 432 | hooks: _t.HooksInputType | None = None, |
| 433 | json: _t.JsonType = None, |
| 434 | ) -> None: |
| 435 | """Prepares the entire request with the given parameters.""" |
| 436 | |
| 437 | url = cast("_t.UriType", url) |
| 438 | self.prepare_method(method) |
| 439 | self.prepare_url(url, params) |
| 440 | self.prepare_headers(headers) |
| 441 | self.prepare_cookies(cookies) |
| 442 | self.prepare_body(data, files, json) |
| 443 | self.prepare_auth(auth, url) |
| 444 | |
| 445 | # Note that prepare_auth must be last to enable authentication schemes |
| 446 | # such as OAuth to work on a fully prepared request. |
| 447 | |
| 448 | # This MUST go after prepare_auth. Authenticators could add a hook |
| 449 | self.prepare_hooks(hooks) |
| 450 | |
| 451 | def __repr__(self) -> str: |
| 452 | return f"<PreparedRequest [{self.method}]>" |
| 453 | |
| 454 | def copy(self) -> PreparedRequest: |
| 455 | p = PreparedRequest() |
| 456 | p.method = self.method |
| 457 | p.url = self.url |
| 458 | p.headers = self.headers.copy() if self.headers is not None else None # type: ignore[assignment] |
| 459 | p._cookies = _copy_cookie_jar(self._cookies) |
| 460 | p.body = self.body |
| 461 | p.hooks = self.hooks |
| 462 | p._body_position = self._body_position |
| 463 | return p |
| 464 | |
| 465 | def prepare_method(self, method: str | None) -> None: |
| 466 | """Prepares the given HTTP method.""" |
| 467 | self.method = method |
| 468 | if self.method is not None: |
| 469 | self.method = to_native_string(self.method.upper()) |
| 470 | |
| 471 | @staticmethod |
| 472 | def _get_idna_encoded_host(host: str) -> str: |
| 473 | import idna |
| 474 | |
| 475 | try: |
| 476 | host = idna.encode(host, uts46=True).decode("utf-8") |
| 477 | except idna.IDNAError: |
| 478 | raise UnicodeError |
| 479 | return host |
| 480 | |
| 481 | def prepare_url( |
| 482 | self, |
| 483 | url: _t.UriType, |
| 484 | params: _t.ParamsType, |
| 485 | ) -> None: |
| 486 | """Prepares the given HTTP URL.""" |
| 487 | #: Accept objects that have string representations. |
| 488 | #: We're unable to blindly call unicode/str functions |
| 489 | #: as this will include the bytestring indicator (b'') |
| 490 | #: on python 3.x. |
| 491 | #: https://github.com/psf/requests/pull/2238 |
| 492 | if isinstance(url, bytes): |
| 493 | url = url.decode("utf8") |
| 494 | else: |
| 495 | url = str(url) |
| 496 | |
| 497 | # Remove leading whitespaces from url |
| 498 | url = url.lstrip() |
| 499 | |
| 500 | # Don't do any URL preparation for non-HTTP schemes like `mailto`, |
| 501 | # `data` etc to work around exceptions from `url_parse`, which |
| 502 | # handles RFC 3986 only. |
| 503 | if ":" in url and not url.lower().startswith("http"): |
| 504 | self.url = url |
| 505 | return |
| 506 | |
| 507 | # Support for unicode domain names and paths. |
| 508 | try: |
| 509 | scheme, auth, host, port, path, query, fragment = parse_url(url) |
| 510 | except LocationParseError as e: |
| 511 | raise InvalidURL(*e.args) |
| 512 | |
| 513 | if not scheme: |
| 514 | raise MissingSchema( |
| 515 | f"Invalid URL {url!r}: No scheme supplied. " |
| 516 | f"Perhaps you meant https://{url}?" |
| 517 | ) |
| 518 | |
| 519 | if not host: |
| 520 | raise InvalidURL(f"Invalid URL {url!r}: No host supplied") |
| 521 | |
| 522 | # In general, we want to try IDNA encoding the hostname if the string contains |
| 523 | # non-ASCII characters. This allows users to automatically get the correct IDNA |
| 524 | # behaviour. For strings containing only ASCII characters, we need to also verify |
| 525 | # it doesn't start with a wildcard (*), before allowing the unencoded hostname. |
| 526 | if not unicode_is_ascii(host): |
| 527 | try: |
| 528 | host = self._get_idna_encoded_host(host) |
| 529 | except UnicodeError: |
| 530 | raise InvalidURL("URL has an invalid label.") |
| 531 | elif host.startswith(("*", ".")): |
| 532 | raise InvalidURL("URL has an invalid label.") |
| 533 | |
| 534 | # Carefully reconstruct the network location |
| 535 | netloc = auth or "" |
| 536 | if netloc: |
| 537 | netloc += "@" |
| 538 | netloc += host |
| 539 | if port: |
| 540 | netloc += f":{port}" |
| 541 | |
| 542 | # Bare domains aren't valid URLs. |
| 543 | if not path: |
| 544 | path = "/" |
| 545 | |
| 546 | if isinstance(params, (str, bytes)): |
| 547 | params = to_native_string(params) |
| 548 | |
| 549 | if params is not None: |
| 550 | enc_params = self._encode_params(params) |
| 551 | else: |
| 552 | enc_params = "" |
| 553 | |
| 554 | if enc_params: |
| 555 | if query: |
| 556 | query = f"{query}&{enc_params}" |
| 557 | else: |
| 558 | query = enc_params |
| 559 | |
| 560 | url = requote_uri(urlunparse((scheme, netloc, path, "", query, fragment))) |
| 561 | self.url = url |
| 562 | |
| 563 | def prepare_headers(self, headers: Mapping[str, str | bytes] | None) -> None: |
| 564 | """Prepares the given HTTP headers.""" |
| 565 | |
| 566 | self.headers = CaseInsensitiveDict() |
| 567 | if headers: |
| 568 | for header in headers.items(): |
| 569 | # Raise exception on invalid header value. |
| 570 | check_header_validity(header) |
| 571 | name, value = header |
| 572 | self.headers[to_native_string(name)] = value |
| 573 | |
| 574 | def prepare_body( |
| 575 | self, data: _t.DataType, files: _t.FilesType, json: _t.JsonType = None |
| 576 | ) -> None: |
| 577 | """Prepares the given HTTP body data.""" |
| 578 | |
| 579 | # Check if file, fo, generator, iterator. |
| 580 | # If not, run through normal process. |
| 581 | |
| 582 | # Nottin' on you. |
| 583 | body = None |
| 584 | content_type = None |
| 585 | |
| 586 | if not data and json is not None: |
| 587 | # urllib3 requires a bytes-like body. Python 2's json.dumps |
| 588 | # provides this natively, but Python 3 gives a Unicode string. |
| 589 | content_type = "application/json" |
| 590 | |
| 591 | try: |
| 592 | body = complexjson.dumps(json, allow_nan=False) |
| 593 | except ValueError as ve: |
| 594 | raise InvalidJSONError(ve, request=self) |
| 595 | |
| 596 | if not isinstance(body, bytes): |
| 597 | body = body.encode("utf-8") |
| 598 | |
| 599 | # data that proxies attributes to underlying objects needs hasattr |
| 600 | is_iterable = isinstance(data, Iterable) or hasattr(data, "__iter__") |
| 601 | if is_iterable and not isinstance(data, (str, bytes, list, tuple, Mapping)): |
| 602 | try: |
| 603 | length = super_len(data) |
| 604 | except (TypeError, AttributeError, UnsupportedOperation): |
| 605 | length = None |
| 606 | |
| 607 | body = data |
| 608 | |
| 609 | if getattr(body, "tell", None) is not None: |
| 610 | # Record the current file position before reading. |
| 611 | # This will allow us to rewind a file in the event |
| 612 | # of a redirect. |
| 613 | try: |
| 614 | self._body_position = body.tell() # type: ignore[union-attr] # guarded by getattr check |
| 615 | except OSError: |
| 616 | # This differentiates from None, allowing us to catch |
| 617 | # a failed `tell()` later when trying to rewind the body |
| 618 | self._body_position = object() |
| 619 | |
| 620 | if files: |
| 621 | raise NotImplementedError( |
| 622 | "Streamed bodies and files are mutually exclusive." |
| 623 | ) |
| 624 | |
| 625 | if length: |
| 626 | self.headers["Content-Length"] = builtin_str(length) |
| 627 | else: |
| 628 | self.headers["Transfer-Encoding"] = "chunked" |
| 629 | else: |
| 630 | # After is_stream filtering, remaining data is raw (not streamed) |
| 631 | raw_data = cast("_t.RawDataType | None", data) |
| 632 | |
| 633 | # Multi-part file uploads. |
| 634 | if files: |
| 635 | (body, content_type) = self._encode_files(files, raw_data) |
| 636 | else: |
| 637 | if raw_data: |
| 638 | body = self._encode_params(raw_data) |
| 639 | if isinstance(data, basestring) or isinstance(data, _SupportsRead): |
| 640 | content_type = None |
| 641 | else: |
| 642 | content_type = "application/x-www-form-urlencoded" |
| 643 | |
| 644 | self.prepare_content_length(body) |
| 645 | |
| 646 | # Add content-type if it wasn't explicitly provided. |
| 647 | if content_type and ("content-type" not in self.headers): |
| 648 | self.headers["Content-Type"] = content_type |
| 649 | |
| 650 | self.body = body # type: ignore[assignment] # body transforms from DataType to BodyType |
| 651 | |
| 652 | def prepare_content_length(self, body: _t.BodyType) -> None: |
| 653 | """Prepare Content-Length header based on request method and body""" |
| 654 | if body is not None: |
| 655 | length = super_len(body) |
| 656 | if length: |
| 657 | # If length exists, set it. Otherwise, we fallback |
| 658 | # to Transfer-Encoding: chunked. |
| 659 | self.headers["Content-Length"] = builtin_str(length) |
| 660 | elif ( |
| 661 | self.method not in ("GET", "HEAD") |
| 662 | and self.headers.get("Content-Length") is None |
| 663 | ): |
| 664 | # Set Content-Length to 0 for methods that can have a body |
| 665 | # but don't provide one. (i.e. not GET or HEAD) |
| 666 | self.headers["Content-Length"] = "0" |
| 667 | |
| 668 | def prepare_auth( |
| 669 | self, |
| 670 | auth: _t.AuthType, |
| 671 | url: _t.UriType = "", |
| 672 | ) -> None: |
| 673 | """Prepares the given HTTP auth data.""" |
| 674 | |
| 675 | # If no Auth is explicitly provided, extract it from the URL first. |
| 676 | if auth is None: |
| 677 | url_auth = get_auth_from_url(cast(str, self.url)) |
| 678 | auth = url_auth if any(url_auth) else None |
| 679 | |
| 680 | if auth: |
| 681 | if isinstance(auth, tuple) and len(auth) == 2: # type: ignore[arg-type] # pyright widens tuple from Callable in AuthType |
| 682 | # special-case basic HTTP auth |
| 683 | auth_handler = HTTPBasicAuth(*auth) # type: ignore[arg-type] # pyright widens tuple from Callable in AuthType |
| 684 | else: |
| 685 | # TODO: can be fixed by flipping the conditionals |
| 686 | auth_handler = cast("Callable[..., PreparedRequest]", auth) |
| 687 | |
| 688 | # Allow auth to make its changes. |
| 689 | r = auth_handler(self) |
| 690 | |
| 691 | # Update self to reflect the auth changes. |
| 692 | self.__dict__.update(r.__dict__) |
| 693 | |
| 694 | # Recompute Content-Length |
| 695 | self.prepare_content_length(self.body) |
| 696 | |
| 697 | def prepare_cookies( |
| 698 | self, cookies: RequestsCookieJar | CookieJar | dict[str, str] | None |
| 699 | ) -> None: |
| 700 | """Prepares the given HTTP cookie data. |
| 701 | |
| 702 | This function eventually generates a ``Cookie`` header from the |
| 703 | given cookies using cookielib. Due to cookielib's design, the header |
| 704 | will not be regenerated if it already exists, meaning this function |
| 705 | can only be called once for the life of the |
| 706 | :class:`PreparedRequest <PreparedRequest>` object. Any subsequent calls |
| 707 | to ``prepare_cookies`` will have no actual effect, unless the "Cookie" |
| 708 | header is removed beforehand. |
| 709 | """ |
| 710 | if isinstance(cookies, cookielib.CookieJar): |
| 711 | self._cookies = cookies |
| 712 | else: |
| 713 | self._cookies = cookiejar_from_dict(cookies) |
| 714 | |
| 715 | cookies_jar = cast("CookieJar", self._cookies) |
| 716 | cookie_header = get_cookie_header(cookies_jar, self) |
| 717 | if cookie_header is not None: |
| 718 | self.headers["Cookie"] = cookie_header |
| 719 | |
| 720 | def prepare_hooks(self, hooks: _t.HooksInputType | None) -> None: |
| 721 | """Prepares the given hooks.""" |
| 722 | # hooks can be passed as None to the prepare method and to this |
| 723 | # method. To prevent iterating over None, simply use an empty list |
| 724 | # if hooks is False-y |
| 725 | hooks = hooks or {} |
| 726 | for event in hooks: |
| 727 | self.register_hook(event, hooks[event]) |
| 728 | |
| 729 | |
| 730 | class Response: |
| 731 | """The :class:`Response <Response>` object, which contains a |
| 732 | server's response to an HTTP request. |
| 733 | """ |
| 734 | |
| 735 | _content: bytes | Literal[False] | None |
| 736 | _content_consumed: bool |
| 737 | _next: PreparedRequest | None |
| 738 | status_code: int |
| 739 | headers: CaseInsensitiveDict[str] |
| 740 | raw: Any |
| 741 | url: str |
| 742 | encoding: str | None |
| 743 | history: list[Response] |
| 744 | reason: str |
| 745 | cookies: RequestsCookieJar |
| 746 | elapsed: datetime.timedelta |
| 747 | request: PreparedRequest |
| 748 | connection: HTTPAdapter |
| 749 | |
| 750 | __attrs__: list[str] = [ |
| 751 | "_content", |
| 752 | "status_code", |
| 753 | "headers", |
| 754 | "url", |
| 755 | "history", |
| 756 | "encoding", |
| 757 | "reason", |
| 758 | "cookies", |
| 759 | "elapsed", |
| 760 | "request", |
| 761 | ] |
| 762 | |
| 763 | def __init__(self) -> None: |
| 764 | self._content = False |
| 765 | self._content_consumed = False |
| 766 | self._next = None |
| 767 | |
| 768 | #: Integer Code of responded HTTP Status, e.g. 404 or 200. |
| 769 | self.status_code = None # type: ignore[assignment] |
| 770 | |
| 771 | #: Case-insensitive Dictionary of Response Headers. |
| 772 | #: For example, ``headers['content-encoding']`` will return the |
| 773 | #: value of a ``'Content-Encoding'`` response header. |
| 774 | self.headers = CaseInsensitiveDict() |
| 775 | |
| 776 | #: File-like object representation of response (for advanced usage). |
| 777 | #: Use of ``raw`` requires that ``stream=True`` be set on the request. |
| 778 | #: This requirement does not apply for use internally to Requests. |
| 779 | self.raw = None |
| 780 | |
| 781 | #: Final URL location of Response. |
| 782 | self.url = None # type: ignore[assignment] |
| 783 | |
| 784 | #: Encoding to decode with when accessing r.text. |
| 785 | self.encoding = None |
| 786 | |
| 787 | #: A list of :class:`Response <Response>` objects from |
| 788 | #: the history of the Request. Any redirect responses will end |
| 789 | #: up here. The list is sorted from the oldest to the most recent request. |
| 790 | self.history = [] |
| 791 | |
| 792 | #: Textual reason of responded HTTP Status, e.g. "Not Found" or "OK". |
| 793 | self.reason = None # type: ignore[assignment] |
| 794 | |
| 795 | #: A CookieJar of Cookies the server sent back. |
| 796 | self.cookies = cookiejar_from_dict({}) |
| 797 | |
| 798 | #: The amount of time elapsed between sending the request |
| 799 | #: and the arrival of the response (as a timedelta). |
| 800 | #: This property specifically measures the time taken between sending |
| 801 | #: the first byte of the request and finishing parsing the headers. It |
| 802 | #: is therefore unaffected by consuming the response content or the |
| 803 | #: value of the ``stream`` keyword argument. |
| 804 | self.elapsed = datetime.timedelta(0) |
| 805 | |
| 806 | #: The :class:`PreparedRequest <PreparedRequest>` object to which this |
| 807 | #: is a response. |
| 808 | self.request = None # type: ignore[assignment] |
| 809 | |
| 810 | def __enter__(self) -> Self: |
| 811 | return self |
| 812 | |
| 813 | def __exit__(self, *args: Any) -> None: |
| 814 | self.close() |
| 815 | |
| 816 | def __getstate__(self) -> dict[str, Any]: |
| 817 | # Consume everything; accessing the content attribute makes |
| 818 | # sure the content has been fully read. |
| 819 | if not self._content_consumed: |
| 820 | self.content |
| 821 | |
| 822 | return {attr: getattr(self, attr, None) for attr in self.__attrs__} |
| 823 | |
| 824 | def __setstate__(self, state: dict[str, Any]) -> None: |
| 825 | for name, value in state.items(): |
| 826 | setattr(self, name, value) |
| 827 | |
| 828 | # pickled objects do not have .raw |
| 829 | setattr(self, "_content_consumed", True) |
| 830 | setattr(self, "raw", None) |
| 831 | |
| 832 | def __repr__(self) -> str: |
| 833 | return f"<Response [{self.status_code}]>" |
| 834 | |
| 835 | def __bool__(self) -> bool: |
| 836 | """Returns True if :attr:`status_code` is less than 400. |
| 837 | |
| 838 | This attribute checks if the status code of the response is between |
| 839 | 400 and 600 to see if there was a client error or a server error. If |
| 840 | the status code, is between 200 and 400, this will return True. This |
| 841 | is **not** a check to see if the response code is ``200 OK``. |
| 842 | """ |
| 843 | return self.ok |
| 844 | |
| 845 | def __nonzero__(self) -> bool: |
| 846 | """Returns True if :attr:`status_code` is less than 400. |
| 847 | |
| 848 | This attribute checks if the status code of the response is between |
| 849 | 400 and 600 to see if there was a client error or a server error. If |
| 850 | the status code, is between 200 and 400, this will return True. This |
| 851 | is **not** a check to see if the response code is ``200 OK``. |
| 852 | """ |
| 853 | return self.ok |
| 854 | |
| 855 | def __iter__(self) -> Iterator[bytes]: |
| 856 | """Allows you to use a response as an iterator.""" |
| 857 | return self.iter_content(128) |
| 858 | |
| 859 | @property |
| 860 | def ok(self) -> bool: |
| 861 | """Returns True if :attr:`status_code` is less than 400, False if not. |
| 862 | |
| 863 | This attribute checks if the status code of the response is between |
| 864 | 400 and 600 to see if there was a client error or a server error. If |
| 865 | the status code is between 200 and 400, this will return True. This |
| 866 | is **not** a check to see if the response code is ``200 OK``. |
| 867 | """ |
| 868 | try: |
| 869 | self.raise_for_status() |
| 870 | except HTTPError: |
| 871 | return False |
| 872 | return True |
| 873 | |
| 874 | @property |
| 875 | def is_redirect(self) -> bool: |
| 876 | """True if this Response is a well-formed HTTP redirect that could have |
| 877 | been processed automatically (by :meth:`Session.resolve_redirects`). |
| 878 | """ |
| 879 | return "location" in self.headers and self.status_code in REDIRECT_STATI |
| 880 | |
| 881 | @property |
| 882 | def is_permanent_redirect(self) -> bool: |
| 883 | """True if this Response one of the permanent versions of redirect.""" |
| 884 | return "location" in self.headers and self.status_code in ( |
| 885 | codes.moved_permanently, |
| 886 | codes.permanent_redirect, |
| 887 | ) |
| 888 | |
| 889 | @property |
| 890 | def next(self) -> PreparedRequest | None: |
| 891 | """Returns a PreparedRequest for the next request in a redirect chain, if there is one.""" |
| 892 | return self._next |
| 893 | |
| 894 | @property |
| 895 | def apparent_encoding(self) -> str | None: |
| 896 | """The apparent encoding, provided by the charset_normalizer or chardet libraries.""" |
| 897 | if chardet is not None: |
| 898 | return chardet.detect(self.content)["encoding"] |
| 899 | else: |
| 900 | # If no character detection library is available, we'll fall back |
| 901 | # to a standard Python utf-8 str. |
| 902 | return "utf-8" |
| 903 | |
| 904 | @overload |
| 905 | def iter_content( |
| 906 | self, chunk_size: int | None = 1, decode_unicode: Literal[False] = False |
| 907 | ) -> Iterator[bytes]: ... |
| 908 | @overload |
| 909 | def iter_content( |
| 910 | self, chunk_size: int | None = 1, *, decode_unicode: Literal[True] |
| 911 | ) -> Iterator[str | bytes]: ... |
| 912 | def iter_content( |
| 913 | self, chunk_size: int | None = 1, decode_unicode: bool = False |
| 914 | ) -> Iterator[str | bytes]: |
| 915 | """Iterates over the response data. When stream=True is set on the |
| 916 | request, this avoids reading the content at once into memory for |
| 917 | large responses. The chunk size is the number of bytes it should |
| 918 | read into memory. This is not necessarily the length of each item |
| 919 | returned as decoding can take place. |
| 920 | |
| 921 | chunk_size must be of type int or None. A value of None will |
| 922 | function differently depending on the value of `stream`. |
| 923 | stream=True will read data as it arrives in whatever size the |
| 924 | chunks are received. If stream=False, data is returned as |
| 925 | a single chunk. |
| 926 | |
| 927 | If decode_unicode is True, content will be decoded using encoding |
| 928 | information from the response. If no encoding information is available, |
| 929 | bytes will be returned. This can be bypassed by manually setting |
| 930 | `encoding` on the response. |
| 931 | """ |
| 932 | |
| 933 | def generate() -> Generator[bytes, None, None]: |
| 934 | # Special case for urllib3. |
| 935 | if hasattr(self.raw, "stream"): |
| 936 | try: |
| 937 | yield from self.raw.stream(chunk_size, decode_content=True) |
| 938 | except ProtocolError as e: |
| 939 | raise ChunkedEncodingError(e) |
| 940 | except DecodeError as e: |
| 941 | raise ContentDecodingError(e) |
| 942 | except ReadTimeoutError as e: |
| 943 | raise ConnectionError(e) |
| 944 | except SSLError as e: |
| 945 | raise RequestsSSLError(e) |
| 946 | else: |
| 947 | # Standard file-like object. |
| 948 | while True: |
| 949 | chunk = self.raw.read(chunk_size) |
| 950 | if not chunk: |
| 951 | break |
| 952 | yield chunk |
| 953 | |
| 954 | self._content_consumed = True |
| 955 | |
| 956 | if self._content_consumed and isinstance(self._content, bool): |
| 957 | raise StreamConsumedError() |
| 958 | elif chunk_size is not None and not isinstance(chunk_size, int): # type: ignore[reportUnnecessaryIsInstance] # runtime guard for untyped callers |
| 959 | raise TypeError( |
| 960 | f"chunk_size must be an int, it is instead a {type(chunk_size)}." |
| 961 | ) |
| 962 | |
| 963 | if self._content_consumed: |
| 964 | # simulate reading small chunks of the content |
| 965 | content = cast(bytes, self._content) |
| 966 | chunks = iter_slices(content, chunk_size) |
| 967 | else: |
| 968 | chunks = generate() |
| 969 | |
| 970 | if decode_unicode: |
| 971 | chunks = stream_decode_response_unicode(chunks, self) |
| 972 | |
| 973 | return chunks |
| 974 | |
| 975 | @overload |
| 976 | def iter_lines( |
| 977 | self, |
| 978 | chunk_size: int = ITER_CHUNK_SIZE, |
| 979 | decode_unicode: Literal[False] = False, |
| 980 | delimiter: bytes | None = None, |
| 981 | ) -> Iterator[bytes]: ... |
| 982 | @overload |
| 983 | def iter_lines( |
| 984 | self, |
| 985 | chunk_size: int = ITER_CHUNK_SIZE, |
| 986 | *, |
| 987 | decode_unicode: Literal[True], |
| 988 | delimiter: str | bytes | None = None, |
| 989 | ) -> Iterator[str | bytes]: ... |
| 990 | def iter_lines( |
| 991 | self, |
| 992 | chunk_size: int = ITER_CHUNK_SIZE, |
| 993 | decode_unicode: bool = False, |
| 994 | delimiter: str | bytes | None = None, |
| 995 | ) -> Iterator[str | bytes]: |
| 996 | """Iterates over the response data, one line at a time. When |
| 997 | stream=True is set on the request, this avoids reading the |
| 998 | content at once into memory for large responses. |
| 999 | |
| 1000 | The decode_unicode param works the same as in `iter_content`, with the |
| 1001 | same caveats. |
| 1002 | |
| 1003 | .. note:: This method is not reentrant safe. |
| 1004 | """ |
| 1005 | |
| 1006 | pending: str | bytes | None = None |
| 1007 | |
| 1008 | for chunk in self.iter_content( |
| 1009 | chunk_size=chunk_size, decode_unicode=decode_unicode |
| 1010 | ): |
| 1011 | if pending is not None: |
| 1012 | # TODO: remove cast after iter_lines rewrite |
| 1013 | chunk = cast("str | bytes", pending + chunk) # type: ignore[operator] |
| 1014 | |
| 1015 | if delimiter: |
| 1016 | lines = chunk.split(delimiter) # type: ignore[arg-type] |
| 1017 | else: |
| 1018 | lines = chunk.splitlines() |
| 1019 | |
| 1020 | if lines and lines[-1] and chunk and lines[-1][-1] == chunk[-1]: |
| 1021 | pending = lines.pop() |
| 1022 | else: |
| 1023 | pending = None |
| 1024 | |
| 1025 | yield from lines |
| 1026 | |
| 1027 | if pending is not None: |
| 1028 | yield pending |
| 1029 | |
| 1030 | @property |
| 1031 | def content(self) -> bytes: |
| 1032 | """Content of the response, in bytes.""" |
| 1033 | |
| 1034 | if self._content is False: |
| 1035 | # Read the contents. |
| 1036 | if self._content_consumed: |
| 1037 | raise RuntimeError("The content for this response was already consumed") |
| 1038 | |
| 1039 | if self.status_code == 0 or self.raw is None: |
| 1040 | self._content = None |
| 1041 | else: |
| 1042 | self._content = b"".join(self.iter_content(CONTENT_CHUNK_SIZE)) or b"" |
| 1043 | |
| 1044 | self._content_consumed = True |
| 1045 | # don't need to release the connection; that's been handled by urllib3 |
| 1046 | # since we exhausted the data. |
| 1047 | return self._content # type: ignore[return-value] |
| 1048 | |
| 1049 | @property |
| 1050 | def text(self) -> str: |
| 1051 | """Content of the response, in unicode. |
| 1052 | |
| 1053 | If Response.encoding is None, encoding will be guessed using |
| 1054 | ``charset_normalizer`` or ``chardet``. |
| 1055 | |
| 1056 | The encoding of the response content is determined based solely on HTTP |
| 1057 | headers, following RFC 2616 to the letter. If you can take advantage of |
| 1058 | non-HTTP knowledge to make a better guess at the encoding, you should |
| 1059 | set ``r.encoding`` appropriately before accessing this property. |
| 1060 | """ |
| 1061 | |
| 1062 | # Try charset from content-type |
| 1063 | content = None |
| 1064 | encoding = self.encoding |
| 1065 | |
| 1066 | if not self.content: |
| 1067 | return "" |
| 1068 | |
| 1069 | # Fallback to auto-detected encoding. |
| 1070 | if self.encoding is None: |
| 1071 | encoding = self.apparent_encoding |
| 1072 | |
| 1073 | # Decode unicode from given encoding. |
| 1074 | try: |
| 1075 | content = str(self.content, encoding or "utf-8", errors="replace") |
| 1076 | except (LookupError, TypeError): |
| 1077 | # A LookupError is raised if the encoding was not found which could |
| 1078 | # indicate a misspelling or similar mistake. |
| 1079 | # |
| 1080 | # A TypeError can be raised if encoding is None |
| 1081 | # |
| 1082 | # So we try blindly encoding. |
| 1083 | content = str(self.content, errors="replace") |
| 1084 | |
| 1085 | return content |
| 1086 | |
| 1087 | def json(self, **kwargs: Any) -> Any: |
| 1088 | r"""Decodes the JSON response body (if any) as a Python object. |
| 1089 | |
| 1090 | This may return a dictionary, list, etc. depending on what is in the response. |
| 1091 | |
| 1092 | :param \*\*kwargs: Optional arguments that ``json.loads`` takes. |
| 1093 | :raises requests.exceptions.JSONDecodeError: If the response body does not |
| 1094 | contain valid json. |
| 1095 | """ |
| 1096 | |
| 1097 | if not self.encoding and self.content and len(self.content) > 3: |
| 1098 | # No encoding set. JSON RFC 4627 section 3 states we should expect |
| 1099 | # UTF-8, -16 or -32. Detect which one to use; If the detection or |
| 1100 | # decoding fails, fall back to `self.text` (using charset_normalizer to make |
| 1101 | # a best guess). |
| 1102 | encoding = guess_json_utf(self.content) |
| 1103 | if encoding is not None: |
| 1104 | try: |
| 1105 | return complexjson.loads(self.content.decode(encoding), **kwargs) |
| 1106 | except UnicodeDecodeError: |
| 1107 | # Wrong UTF codec detected; usually because it's not UTF-8 |
| 1108 | # but some other 8-bit codec. This is an RFC violation, |
| 1109 | # and the server didn't bother to tell us what codec *was* |
| 1110 | # used. |
| 1111 | pass |
| 1112 | except JSONDecodeError as e: |
| 1113 | raise RequestsJSONDecodeError(e.msg, e.doc, e.pos) |
| 1114 | |
| 1115 | try: |
| 1116 | return complexjson.loads(self.text, **kwargs) |
| 1117 | except JSONDecodeError as e: |
| 1118 | # Catch JSON-related errors and raise as requests.JSONDecodeError |
| 1119 | # This aliases json.JSONDecodeError and simplejson.JSONDecodeError |
| 1120 | raise RequestsJSONDecodeError(e.msg, e.doc, e.pos) |
| 1121 | |
| 1122 | @property |
| 1123 | def links(self) -> dict[str, dict[str, str]]: |
| 1124 | """Returns the parsed header links of the response, if any.""" |
| 1125 | |
| 1126 | header = self.headers.get("link") |
| 1127 | |
| 1128 | resolved_links: dict[str, dict[str, str]] = {} |
| 1129 | |
| 1130 | if header: |
| 1131 | links = parse_header_links(header) |
| 1132 | |
| 1133 | for link in links: |
| 1134 | key = link.get("rel") or link.get("url") |
| 1135 | if key is not None: |
| 1136 | resolved_links[key] = link |
| 1137 | |
| 1138 | return resolved_links |
| 1139 | |
| 1140 | def raise_for_status(self) -> None: |
| 1141 | """Raises :class:`HTTPError`, if one occurred.""" |
| 1142 | |
| 1143 | http_error_msg = "" |
| 1144 | if isinstance(self.reason, bytes): |
| 1145 | # We attempt to decode utf-8 first because some servers |
| 1146 | # choose to localize their reason strings. If the string |
| 1147 | # isn't utf-8, we fall back to iso-8859-1 for all other |
| 1148 | # encodings. (See PR #3538) |
| 1149 | try: |
| 1150 | reason = self.reason.decode("utf-8") |
| 1151 | except UnicodeDecodeError: |
| 1152 | reason = self.reason.decode("iso-8859-1") |
| 1153 | else: |
| 1154 | reason = self.reason |
| 1155 | |
| 1156 | if 400 <= self.status_code < 500: |
| 1157 | http_error_msg = ( |
| 1158 | f"{self.status_code} Client Error: {reason} for url: {self.url}" |
| 1159 | ) |
| 1160 | |
| 1161 | elif 500 <= self.status_code < 600: |
| 1162 | http_error_msg = ( |
| 1163 | f"{self.status_code} Server Error: {reason} for url: {self.url}" |
| 1164 | ) |
| 1165 | |
| 1166 | if http_error_msg: |
| 1167 | raise HTTPError(http_error_msg, response=self) |
| 1168 | |
| 1169 | def close(self) -> None: |
| 1170 | """Releases the connection back to the pool. Once this method has been |
| 1171 | called the underlying ``raw`` object must not be accessed again. |
| 1172 | |
| 1173 | *Note: Should not normally need to be called explicitly.* |
| 1174 | """ |
| 1175 | if not self._content_consumed: |
| 1176 | self.raw.close() |
| 1177 | |
| 1178 | release_conn = getattr(self.raw, "release_conn", None) |
| 1179 | if release_conn is not None: |
| 1180 | release_conn() |
| 1181 |