Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/urllib3/util/request.py: 34%
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 io
4import sys
5import typing
6from base64 import b64encode
7from enum import Enum
9from ..exceptions import UnrewindableBodyError
10from .util import to_bytes
12if typing.TYPE_CHECKING:
13 from typing import Final
15# Pass as a value within ``headers`` to skip
16# emitting some HTTP headers that are added automatically.
17# The only headers that are supported are ``Accept-Encoding``,
18# ``Host``, and ``User-Agent``.
19SKIP_HEADER = "@@@SKIP_HEADER@@@"
20SKIPPABLE_HEADERS = frozenset(["accept-encoding", "host", "user-agent"])
22ACCEPT_ENCODING = "gzip,deflate"
23try:
24 try:
25 import brotlicffi as _unused_module_brotli # type: ignore[import-not-found] # noqa: F401
26 except ImportError:
27 import brotli as _unused_module_brotli # type: ignore[import-not-found] # noqa: F401
28except ImportError:
29 pass
30else:
31 ACCEPT_ENCODING += ",br"
33try:
34 if sys.version_info >= (3, 14):
35 from compression import zstd as _unused_module_zstd # noqa: F401
36 else:
37 from backports import zstd as _unused_module_zstd # noqa: F401
38except ImportError:
39 pass
40else:
41 ACCEPT_ENCODING += ",zstd"
44class _TYPE_FAILEDTELL(Enum):
45 token = 0
48_FAILEDTELL: Final[_TYPE_FAILEDTELL] = _TYPE_FAILEDTELL.token
50_TYPE_BODY_POSITION = typing.Union[int, _TYPE_FAILEDTELL]
52# When sending a request with these methods we aren't expecting
53# a body so don't need to set an explicit 'Content-Length: 0'
54# The reason we do this in the negative instead of tracking methods
55# which 'should' have a body is because unknown methods should be
56# treated as if they were 'POST' which *does* expect a body.
57_METHODS_NOT_EXPECTING_BODY = {"GET", "HEAD", "DELETE", "TRACE", "OPTIONS", "CONNECT"}
60def make_headers(
61 keep_alive: bool | None = None,
62 accept_encoding: bool | list[str] | str | None = None,
63 user_agent: str | None = None,
64 basic_auth: str | None = None,
65 proxy_basic_auth: str | None = None,
66 disable_cache: bool | None = None,
67 *,
68 basic_auth_encoding: str = "latin-1",
69 proxy_basic_auth_encoding: str = "latin-1",
70) -> dict[str, str]:
71 """
72 Shortcuts for generating request headers.
74 :param keep_alive:
75 If ``True``, adds 'connection: keep-alive' header.
77 :param accept_encoding:
78 Can be a boolean, list, or string.
79 ``True`` translates to 'gzip,deflate'. If the dependencies for
80 Brotli (either the ``brotli`` or ``brotlicffi`` package) and/or
81 Zstandard (the ``backports.zstd`` package for Python before 3.14)
82 algorithms are installed, then their encodings are
83 included in the string ('br' and 'zstd', respectively).
84 List will get joined by comma.
85 String will be used as provided.
87 :param user_agent:
88 String representing the user-agent you want, such as
89 "python-urllib3/0.6"
91 :param basic_auth:
92 Colon-separated username:password string for 'authorization: basic ...'
93 auth header. Basic authentication transmits Base64-encoded bytes, not
94 Unicode text, so the string is encoded with ``basic_auth_encoding``
95 before Base64 encoding.
97 Must contain decoded credentials (not percent-encoded).
98 For URLs parsed with :func:`parse_url`, use :attr:`Url.auth_decoded_joined`
99 to get the properly formatted string.
101 :param basic_auth_encoding:
102 Text encoding used to encode ``basic_auth`` before Base64 encoding.
103 Defaults to "latin-1" for backward compatibility. Use "utf-8" only
104 when the peer expects UTF-8 Basic authentication credentials.
106 Must contain decoded credentials (not percent-encoded).
107 For URLs parsed with :func:`parse_url`, use :attr:`Url.auth_decoded_joined`
108 to get the properly formatted string.
110 :param proxy_basic_auth:
111 Colon-separated username:password string for 'proxy-authorization: basic ...'
112 auth header. Basic authentication transmits Base64-encoded bytes, not
113 Unicode text, so the string is encoded with ``proxy_basic_auth_encoding``
114 before Base64 encoding.
116 Must contain decoded credentials (not percent-encoded).
117 For URLs parsed with :func:`parse_url`, use :attr:`Url.auth_decoded_joined`
118 to get the properly formatted string.
120 :param proxy_basic_auth_encoding:
121 Text encoding used to encode ``proxy_basic_auth`` before Base64
122 encoding. Defaults to "latin-1" for backward compatibility. Use
123 "utf-8" only when the proxy expects UTF-8 Basic authentication
124 credentials.
126 Must contain decoded credentials (not percent-encoded).
127 For URLs parsed with :func:`parse_url`, use :attr:`Url.auth_decoded_joined`
128 to get the properly formatted string.
130 :param disable_cache:
131 If ``True``, adds 'cache-control: no-cache' header.
133 Example:
135 .. code-block:: python
137 import urllib3
139 print(urllib3.util.make_headers(keep_alive=True, user_agent="Batman/1.0"))
140 # {'connection': 'keep-alive', 'user-agent': 'Batman/1.0'}
141 print(urllib3.util.make_headers(accept_encoding=True))
142 # {'accept-encoding': 'gzip,deflate'}
143 """
144 headers: dict[str, str] = {}
145 if accept_encoding:
146 if isinstance(accept_encoding, str):
147 pass
148 elif isinstance(accept_encoding, list):
149 accept_encoding = ",".join(accept_encoding)
150 else:
151 accept_encoding = ACCEPT_ENCODING
152 headers["accept-encoding"] = accept_encoding
154 if user_agent:
155 headers["user-agent"] = user_agent
157 if keep_alive:
158 headers["connection"] = "keep-alive"
160 if basic_auth:
161 headers["authorization"] = (
162 f"Basic {b64encode(basic_auth.encode(basic_auth_encoding)).decode()}"
163 )
165 if proxy_basic_auth:
166 headers["proxy-authorization"] = (
167 f"Basic {b64encode(proxy_basic_auth.encode(proxy_basic_auth_encoding)).decode()}"
168 )
170 if disable_cache:
171 headers["cache-control"] = "no-cache"
173 return headers
176def set_file_position(
177 body: typing.Any, pos: _TYPE_BODY_POSITION | None
178) -> _TYPE_BODY_POSITION | None:
179 """
180 If a position is provided, move file to that point.
181 Otherwise, we'll attempt to record a position for future use.
182 """
183 if pos is not None:
184 rewind_body(body, pos)
185 elif getattr(body, "tell", None) is not None:
186 try:
187 pos = body.tell()
188 except OSError:
189 # This differentiates from None, allowing us to catch
190 # a failed `tell()` later when trying to rewind the body.
191 pos = _FAILEDTELL
193 return pos
196def rewind_body(body: typing.IO[typing.AnyStr], body_pos: _TYPE_BODY_POSITION) -> None:
197 """
198 Attempt to rewind body to a certain position.
199 Primarily used for request redirects and retries.
201 :param body:
202 File-like object that supports seek.
204 :param int pos:
205 Position to seek to in file.
206 """
207 body_seek = getattr(body, "seek", None)
208 if body_seek is not None and isinstance(body_pos, int):
209 try:
210 body_seek(body_pos)
211 except OSError as e:
212 raise UnrewindableBodyError(
213 "An error occurred when rewinding request body for redirect/retry."
214 ) from e
215 elif body_pos is _FAILEDTELL:
216 raise UnrewindableBodyError(
217 "Unable to record file position for rewinding "
218 "request body during a redirect/retry."
219 )
220 else:
221 raise ValueError(
222 f"body_pos must be of type integer, instead it was {type(body_pos)}."
223 )
226class ChunksAndContentLength(typing.NamedTuple):
227 chunks: typing.Iterable[bytes] | None
228 content_length: int | None
231def body_to_chunks(
232 body: typing.Any | None, method: str, blocksize: int
233) -> ChunksAndContentLength:
234 """Takes the HTTP request method, body, and blocksize and
235 transforms them into an iterable of chunks to pass to
236 socket.sendall() and an optional 'Content-Length' header.
238 A 'Content-Length' of 'None' indicates the length of the body
239 can't be determined so should use 'Transfer-Encoding: chunked'
240 for framing instead.
241 """
243 chunks: typing.Iterable[bytes] | None
244 content_length: int | None
246 # No body, we need to make a recommendation on 'Content-Length'
247 # based on whether that request method is expected to have
248 # a body or not.
249 if body is None:
250 chunks = None
251 if method.upper() not in _METHODS_NOT_EXPECTING_BODY:
252 content_length = 0
253 else:
254 content_length = None
256 # Bytes or strings become bytes
257 elif isinstance(body, (str, bytes)):
258 chunks = (to_bytes(body),)
259 content_length = len(chunks[0])
261 # File-like object, TODO: use seek() and tell() for length?
262 elif hasattr(body, "read"):
264 def chunk_readable() -> typing.Iterable[bytes]:
265 encode = isinstance(body, io.TextIOBase)
266 while True:
267 datablock = body.read(blocksize)
268 if not datablock:
269 break
270 if encode:
271 datablock = datablock.encode("utf-8")
272 yield datablock
274 chunks = chunk_readable()
275 content_length = None
277 # Otherwise we need to start checking via duck-typing.
278 else:
279 try:
280 # Check if the body implements the buffer API.
281 mv = memoryview(body)
282 except TypeError:
283 try:
284 # Check if the body is an iterable
285 chunks = iter(body)
286 content_length = None
287 except TypeError:
288 raise TypeError(
289 f"'body' must be a bytes-like object, file-like "
290 f"object, or iterable. Instead was {body!r}"
291 ) from None
292 else:
293 # Since it implements the buffer API can be passed directly to socket.sendall()
294 chunks = (body,)
295 content_length = mv.nbytes
297 return ChunksAndContentLength(chunks=chunks, content_length=content_length)