# Requests Administrator & Developer Guide

This document is for the people responsible for *embedding* Requests into a
production system: library integrators, application architects, platform
engineers, security reviewers, and the developers who maintain forks or write
custom transport adapters. The User Guide tells you how to call
`requests.get(url)`. This guide explains what happens after you do — every
hook, every adapter knob, every TLS decision, every redirect rule, every
extension point.

It documents Requests **2.33.1** at commit `514c162`, the current `main`
branch of `psf/requests` as of 2026-04-15. Where the implementation deviates
from the public docs, the source wins; this guide reflects the source.

## 1. Architecture Overview

Requests is a layered HTTP client. The user-facing API is intentionally tiny
(seven verb helpers); the bulk of the implementation is in three classes —
`Session`, `PreparedRequest`/`Response`, and `HTTPAdapter` — that together
implement an opinionated wrapper around `urllib3`.

### 1.1 The big picture

```
+-----------------------------------------------------------------+
|  USER CODE                                                      |
|  requests.get(url) / s.post(url, json=...) / etc.               |
+----------------------+------------------------------------------+
                       v
+-----------------------------------------------------------------+
|  requests.api  (api.py)                                         |
|    - get/post/put/patch/delete/head/options/request             |
|    - All delegate to a one-shot Session via a `with` block      |
+----------------------+------------------------------------------+
                       v
+-----------------------------------------------------------------+
|  Session  (sessions.py)                                         |
|    - Holds defaults: headers, cookies, auth, proxies,           |
|      verify, cert, hooks, params, stream, trust_env,            |
|      max_redirects, adapters dict                               |
|    - request() builds a Request, prepares it, sends             |
|    - send() runs hooks, follows redirects, accumulates history  |
|    - SessionRedirectMixin owns redirect logic                   |
+----------------------+------------------------------------------+
                       v
+-----------------------------------------------------------------+
|  Request -> PreparedRequest  (models.py)                        |
|    - Request: user-facing constructor; .prepare() returns PR    |
|    - PreparedRequest: .method/.url/.headers/.body/.hooks        |
|    - prepare_*() methods normalize URL, headers, body, auth,    |
|      cookies, hooks                                             |
+----------------------+------------------------------------------+
                       v
+-----------------------------------------------------------------+
|  Adapter (adapters.py)                                          |
|    - BaseAdapter contract: send(), close()                      |
|    - HTTPAdapter: real impl, owns urllib3 PoolManager and       |
|      ProxyManager registry                                      |
|    - Mounted by URL prefix on Session.adapters (OrderedDict)    |
+----------------------+------------------------------------------+
                       v
+-----------------------------------------------------------------+
|  urllib3.PoolManager / ProxyManager / SOCKSProxyManager         |
|    - HTTPS connection pooling, retries (Retry), keep-alive,     |
|      gzip/deflate decode, chunked transfer                      |
+----------------------+------------------------------------------+
                       v
+-----------------------------------------------------------------+
|  Standard library: http.client, socket, ssl                      |
+-----------------------------------------------------------------+
```

> [!NOTE]
> Requests does **not** speak HTTP itself. Every byte on the wire is produced
> by `urllib3`. Requests' value lies in its API ergonomics, redirect logic,
> auth handlers, body encoding, encoding detection, hook system, and cookie
> persistence. This is why a single-file `requests.compat` module wraps
> `urllib3` import quirks rather than reinventing them.

### 1.2 Component inventory

| Component | Path | Role |
|---|---|---|
| Top-level API | `src/requests/__init__.py` | Re-exports the public surface; runs version compatibility check on import. |
| Verb helpers | `src/requests/api.py` | `get`, `options`, `head`, `post`, `put`, `patch`, `delete`, `request`. Each opens a one-shot `Session()` via context manager. |
| Sessions | `src/requests/sessions.py` | `Session`, `SessionRedirectMixin`, `merge_setting`, `merge_hooks`, deprecated `session()` shim. |
| Request/Response models | `src/requests/models.py` | `Request`, `PreparedRequest`, `Response`, `RequestEncodingMixin`, `RequestHooksMixin`. |
| Transport adapters | `src/requests/adapters.py` | `BaseAdapter`, `HTTPAdapter`, urllib3 pool/TLS bridging. |
| Authentication | `src/requests/auth.py` | `AuthBase`, `HTTPBasicAuth`, `HTTPProxyAuth`, `HTTPDigestAuth`, `_basic_auth_str`. |
| Cookies | `src/requests/cookies.py` | `RequestsCookieJar`, `MockRequest`, `MockResponse`, `cookiejar_from_dict`, `extract_cookies_to_jar`. |
| Exceptions | `src/requests/exceptions.py` | Full hierarchy rooted at `RequestException(IOError)`; warnings rooted at `RequestsWarning`. |
| Hooks | `src/requests/hooks.py` | `HOOKS = ['response']`, `default_hooks()`, `dispatch_hook()`. |
| Status codes | `src/requests/status_codes.py` | `codes` `LookupDict` with all aliases. |
| Data structures | `src/requests/structures.py` | `CaseInsensitiveDict`, `LookupDict`. |
| Utilities | `src/requests/utils.py` | Proxy resolution, header validation, IDNA helpers, `default_headers()`, `get_netrc_auth()`, `super_len()`. |
| CA bundle | `src/requests/certs.py` | `where()` returns the path to the certifi CA bundle. |
| Compat shim | `src/requests/compat.py` | Reconciles `urllib3` 1.x vs 2.x and `chardet`/`charset_normalizer`. |
| Internal helpers | `src/requests/_internal_utils.py` | `to_native_string`, `unicode_is_ascii`, `HEADER_VALIDATORS`. |
| Bug-report helper | `src/requests/help.py` | `python -m requests.help` — environment dump. |
| Package metadata | `src/requests/__version__.py` | `__title__`, `__version__='2.33.1'`, `__build__=0x023301`, license, author. |
| Vendored re-exports | `src/requests/packages.py` | Compatibility for `requests.packages.urllib3.*` style imports. |

### 1.3 Technology stack

| Layer | Library | Version constraint | Role |
|---|---|---|---|
| HTTP transport | `urllib3` | >= 1.21.1 (2.x supported) | Connection pooling, retries, TLS, chunked I/O. |
| TLS root store | `certifi` | latest | The CA bundle; `requests.certs.where()` returns its path. |
| Charset detection | `charset_normalizer` | >= 2.0, < 4.0 | Default body-encoding inference. |
| Charset detection (alt) | `chardet` | >= 3.0.2, < 8.0.0 | Optional alternative; preferred over `charset_normalizer` if both are installed. |
| IDNA | `idna` | latest | International domain-name encoding for non-ASCII hostnames. |
| Optional SOCKS | `urllib3[socks]` (`PySocks`) | optional | SOCKS proxy support; absence raises `InvalidSchema`. |
| Optional PyOpenSSL | `urllib3.contrib.pyopenssl` | optional | SNI fallback when `ssl.HAS_SNI=False`. |
| Build | `setuptools` | latest | Package builds via `setup.py` shim that just calls `setuptools.setup()`. |

The runtime dependency check runs at `import requests` time. `check_compatibility()`
in `__init__.py` asserts urllib3 >= 1.21.1 and a charset-detection library
within the supported version range; out-of-range versions trigger
`RequestsDependencyWarning` rather than hard failure.

### 1.4 Per-request data flow

The flow for `s.post(url, json={'k': 1})`:

1. **`Session.post(url, ...)`** → `Session.request('POST', url, json=...)`.
2. **Build `Request`** — a `Request` instance is constructed with method,
   url, json, plus session-aware merges of `headers`, `cookies`, `hooks`.
3. **`Session.prepare_request(req)`** — produces a `PreparedRequest`:
   - merges request and session cookies into a fresh `RequestsCookieJar`,
   - applies `.netrc` auth if no explicit auth and `trust_env=True`,
   - calls `PreparedRequest.prepare()`, which in turn runs `prepare_method`,
     `prepare_url` (IDNA, validate scheme/host), `prepare_headers`,
     `prepare_cookies`, `prepare_body` (multipart/json/form/stream),
     `prepare_auth`, and `prepare_hooks`.
4. **`Session.merge_environment_settings(url, proxies, stream, verify, cert)`**
   — folds in env-var proxies (if `trust_env`), `REQUESTS_CA_BUNDLE` /
   `CURL_CA_BUNDLE`, then merges with session defaults via `merge_setting`.
5. **`Session.send(prepared, **send_kwargs)`** —
   - Picks an adapter via `Session.get_adapter(url)` (longest prefix match).
   - Records `start = preferred_clock()`.
   - Calls `adapter.send(prepared, stream, timeout, verify, cert, proxies)`.
   - Records `r.elapsed`.
   - Dispatches the `response` hook chain.
   - Persists cookies via `extract_cookies_to_jar()`.
   - If `allow_redirects` and the response is a redirect, runs
     `SessionRedirectMixin.resolve_redirects()`, accumulating intermediate
     responses into `r.history`.
   - If `not stream`, eagerly reads `r.content` (forces full body into memory).
6. **`HTTPAdapter.send()`** —
   - `get_connection_with_tls_context()` → urllib3 connection from pool.
   - `cert_verify()` configures TLS verification.
   - `request_url()` builds the request-target.
   - `add_headers()` (no-op by default; subclass hook).
   - `conn.urlopen(...)` — actual HTTP exchange.
   - Translates urllib3 exceptions into `requests.exceptions.*`.
   - `build_response(req, resp)` → `Response` with `headers`, `status_code`,
     `encoding`, `cookies`, `request`, `connection=self`.

### 1.5 Why the one-shot Session in `api.py`

Every top-level helper does this:

```python
def request(method, url, **kwargs):
    with sessions.Session() as session:
        return session.request(method=method, url=url, **kwargs)
```

The `with` block guarantees `session.close()` runs and pooled sockets are
released. Without it, repeated `requests.get()` calls would leak file
descriptors when callers ignore the response. This is also why per-call
helpers cannot reuse connections — each call spins up its own pool.

> [!IMPORTANT]
> If your code does more than one HTTP call in a single process lifetime,
> *use a `Session`*. The verb helpers exist for one-liners and notebooks.

## 2. System Requirements

### 2.1 Runtime requirements

| Layer | Version | Source-of-truth |
|---|---|---|
| Python | 3.10+ | `setup.py:if sys.version_info < (3, 10)` raises and exits. |
| `urllib3` | >= 1.21.1 | `__init__.py:check_compatibility()`. |
| `certifi` | latest | `requests.certs.where()` returns its data path. |
| `charset_normalizer` *or* `chardet` | per matrix above | `__init__.py:check_compatibility()`. |
| `idna` | latest | `models.PreparedRequest._get_idna_encoded_host`. |

### 2.2 Optional runtime extras

| Extra | Provides | Behavior when absent |
|---|---|---|
| `urllib3[socks]` (PySocks) | SOCKS4/5 proxies | `InvalidSchema('Missing dependencies for SOCKS support.')`. |
| `urllib3.contrib.pyopenssl` | SNI on legacy CPython | Auto-injected only if `ssl.HAS_SNI` is `False`. |
| `cryptography` | OpenSSL backend for PyOpenSSL | Old (<1.3.4) versions warn `RequestsDependencyWarning`. |

### 2.3 Build & test requirements

The repo ships `pyproject.toml` (PEP 517 build), `MANIFEST.in`, and `Makefile`
for development tasks. `requirements-dev.txt` pins development tooling. The
docs build uses `Sphinx==7.2.6` (pinned in `docs/requirements.txt` to keep
RTD reproducible).

`setup.py` is a thin shim:

```python
import sys
if sys.version_info < (3, 10):
    sys.stderr.write("Requests requires Python 3.10 or later.\n")
    sys.exit(1)
from setuptools import setup
setup()
```

All real metadata (project name, version, deps, classifiers) lives in
`pyproject.toml`. The `__build__ = 0x023301` integer in
`__version__.py` mirrors the version `2.33.1` in 0xMMmmpp form, used by
some downstream tooling for fast version comparison.

## 3. Configuration Reference

There is **no configuration file**. All knobs are call kwargs, session
attributes, or environment variables.

### 3.1 Session attributes (full list)

| Attribute | Type | Default | Notes |
|---|---|---|---|
| `headers` | `CaseInsensitiveDict` | `default_headers()` | UA, Accept-Encoding, Accept, Connection. Merged per-request. |
| `auth` | tuple / `AuthBase` | `None` | Applied if request-level `auth` not set. |
| `proxies` | dict | `{}` | Merged with env proxies if `trust_env`. |
| `hooks` | dict | `{'response': []}` | Only `'response'` event fires. |
| `params` | dict | `{}` | Query string merged into every URL. |
| `verify` | bool / str | `True` | TLS verify mode or CA-bundle path. |
| `cert` | str / tuple | `None` | Client cert (mTLS). |
| `adapters` | OrderedDict | `{'https://': HTTPAdapter, 'http://': HTTPAdapter}` | Sorted by descending prefix length. |
| `stream` | bool | `False` | Per-session default. |
| `trust_env` | bool | `True` | Env vars + netrc only consulted if `True`. |
| `max_redirects` | int | `30` | `DEFAULT_REDIRECT_LIMIT` from `models.py`. |
| `cookies` | `RequestsCookieJar` | empty | Persisted across requests. |

`Session.__attrs__` lists exactly which of these get pickled (everything
above except `cookies` and `adapters`; cookies are persisted explicitly,
adapters are rebuilt on `__setstate__` via `mount()`).

### 3.2 Environment variables (full list)

Read by Requests itself:

| Variable | Read by | Effect |
|---|---|---|
| `REQUESTS_CA_BUNDLE` | `Session.merge_environment_settings()` | Path to CA bundle for TLS verify. |
| `CURL_CA_BUNDLE` | same | Fallback if `REQUESTS_CA_BUNDLE` unset. |
| `NETRC` | `utils.get_netrc_auth()` | Override default netrc location. |

Read by stdlib `urllib.request` (and consulted by Requests via
`get_environ_proxies()`):

| Variable | Effect |
|---|---|
| `HTTP_PROXY`, `http_proxy` | Default HTTP proxy. |
| `HTTPS_PROXY`, `https_proxy` | Default HTTPS proxy. |
| `ALL_PROXY`, `all_proxy` | Default proxy for any scheme. |
| `NO_PROXY`, `no_proxy` | Comma-separated bypass list. |

Suppressed from honoring environment vars: `Session.trust_env = False`.

### 3.3 The full `request()` kwarg reference

Every kwarg `Session.request()` accepts (verb helpers all delegate here):

| Kwarg | Type | Default | Description |
|---|---|---|---|
| `method` | str | required | HTTP method. Case-normalized to upper. |
| `url` | str | required | Absolute URL. Non-HTTP schemes (e.g. `mailto:`) skip URL preparation. |
| `params` | dict / list / bytes / str | `None` | Query parameters. Lists yield repeated keys. |
| `data` | dict / list / bytes / file-like / iterable | `None` | Body. Form-encoded if Mapping. |
| `headers` | dict | `None` | Merged with session headers; `None`-valued keys remove the session value. |
| `cookies` | dict / `CookieJar` | `None` | Merged into a `RequestsCookieJar` over session cookies. |
| `files` | dict | `None` | Multipart parts; values are file-like or 2/3/4-tuples. |
| `auth` | tuple / `AuthBase` | `None` | Replaces session auth. |
| `timeout` | float / tuple | `None` | None = no timeout. Tuple = `(connect, read)`. |
| `allow_redirects` | bool | `True` (False for HEAD) | Follow `301/302/303/307/308`. |
| `proxies` | dict | `None` | Per-call proxies; merged with session and env proxies. |
| `hooks` | dict | `None` | One-shot hooks; merged via `merge_hooks` to preserve session defaults. |
| `stream` | bool | `None` (falls back to session) | If `True`, body not preloaded. |
| `verify` | bool / str | `None` (session default) | Replaces session value. |
| `cert` | str / tuple | `None` (session default) | Replaces session value. |
| `json` | any | `None` | Auto-serialized to body if `data` is also empty. |

### 3.4 `HTTPAdapter` constructor knobs

```python
HTTPAdapter(
    pool_connections=10,    # DEFAULT_POOLSIZE
    pool_maxsize=10,        # DEFAULT_POOLSIZE
    max_retries=0,          # DEFAULT_RETRIES (Retry(0, read=False))
    pool_block=False,       # DEFAULT_POOLBLOCK
)
```

| Kwarg | Default | Meaning |
|---|---|---|
| `pool_connections` | `10` | Number of distinct host pools cached by `urllib3.PoolManager`. |
| `pool_maxsize` | `10` | Max keep-alive connections per pool (per host). |
| `max_retries` | `0` | Either an int (converted via `Retry.from_int`) or a fully-customized `urllib3.Retry`. The default builds `Retry(0, read=False)` — no retries, do not retry idempotent reads. |
| `pool_block` | `False` | If `True`, `acquire()` blocks when no free connection; if `False`, urllib3 raises. |

Most production deployments will mount a custom adapter with a configured
`Retry` policy:

```python
from urllib3.util.retry import Retry
retries = Retry(
    total=5,
    connect=3,
    read=3,
    backoff_factor=0.5,
    status_forcelist=[502, 503, 504],
    allowed_methods=frozenset({'GET', 'HEAD', 'OPTIONS'}),  # urllib3 1.26+ kwarg
    respect_retry_after_header=True,
)
s.mount('https://', HTTPAdapter(max_retries=retries, pool_maxsize=20))
```

> [!IMPORTANT]
> `max_retries` applies *only* to failed DNS lookups, socket connections,
> and connection timeouts. Once data has begun flowing, Requests will not
> retry. Idempotency is the responsibility of the caller.

### 3.5 The `LookupDict` for status codes

`requests.codes` is a `LookupDict` populated at import time from the
`status_codes.py:_codes` table. Every code has at least one alias; common
codes have many. Lookups are attribute-style (`codes.ok`) with both lower-
and upper-case forms registered (`codes.OK`), and dictionary-style
(`codes['ok']`). A missing key returns `None` rather than raising.

The unusual aliases — `codes['\\o/']` for 200, `codes['-o-']` for 404,
`codes['/o\\']` for 500 — are present in the source as Easter eggs from the
original Kenneth Reitz code base.

## 4. Deployment & Scaling

Requests is a library, not a service: there is nothing to "deploy" in the
operational sense. This section is about the pieces of your application that
host Requests-driven HTTP traffic.

### 4.1 Connection pool sizing

The default `HTTPAdapter(pool_connections=10, pool_maxsize=10)` is fine for
most synchronous applications. Increase `pool_maxsize` when:

- A single thread or process makes many concurrent requests to the same host
  (e.g. a worker pool inside one process).
- You see urllib3 `EmptyPoolError` warnings in logs.

Increase `pool_connections` when you talk to many distinct hosts and notice
hosts being evicted from the pool cache.

```python
big = HTTPAdapter(pool_connections=20, pool_maxsize=50, pool_block=False)
s.mount('https://', big)
s.mount('http://',  big)
```

> [!TIP]
> One Session per worker thread (or per asyncio loop, when wrapping in
> `loop.run_in_executor`) is the simplest model that scales. Sharing one
> session across hundreds of threads is supported but contention reduces
> the value of pooling.

### 4.2 Retries and idempotency

The default adapter does not retry. Mount a custom adapter with a
`urllib3.Retry` to enable backoff:

```python
Retry(
    total=5, backoff_factor=0.5, status_forcelist=[429, 502, 503, 504],
    respect_retry_after_header=True,
    allowed_methods=frozenset({'GET', 'HEAD', 'OPTIONS', 'PUT', 'DELETE'}),
)
```

`backoff_factor=0.5` produces sleeps of `0.5 * (2 ** (n-1))` seconds — 0.5,
1, 2, 4, ... — between retries.

> [!WARNING]
> Including non-idempotent methods (`POST`, `PATCH`) in `allowed_methods`
> can cause double-charges, double-sends, or duplicate writes. Default urllib3
> behavior excludes them; do not override unless you have idempotency keys.

### 4.3 DNS, TLS, and process model

Each `urllib3` connection caches the resolved IP. Long-lived sessions on
hosts with rapidly-changing DNS (e.g. AWS internal LBs) may keep stale
addresses. Mitigations: short connection idle timeouts (urllib3-level),
or a periodic `s.close()` to force a fresh pool.

TLS handshakes dominate latency for first-touch requests. Pre-warming a
session by issuing one HEAD/OPTIONS at startup is a common trick to avoid
the cold-start hit on first user traffic.

### 4.4 Process forking

Requests `Session` objects own urllib3 PoolManagers, which own sockets.
Forking after creating a session is unsafe — both child and parent will
believe they own the same fd. **Create sessions after `os.fork()`** (or
inside the child entry point of `multiprocessing.Process`). The same caveat
applies to gunicorn pre-fork workers; create the session inside `post_fork`
or lazily on first use within the worker.

### 4.5 Async / event-loop integration

Requests is synchronous. Calling `requests.get()` from inside an asyncio
coroutine blocks the loop. Two compatible patterns:

- Wrap calls in `asyncio.to_thread()` (Python 3.9+):
  ```python
  await asyncio.to_thread(s.get, url, timeout=5)
  ```
- Switch to `httpx` (semantically similar) or `aiohttp` for true async.

There is no `requests.aio`, no `requests.async`, no `AsyncSession`. The
package surface in `__init__.py` is exactly the one documented here.

## 5. Operations

### 5.1 Logging

Requests itself logs nothing by default — `logging.getLogger(__name__).addHandler(NullHandler())`
in `__init__.py` ensures no output. `urllib3` is the noisy layer; bump it
to `DEBUG` to see the wire:

```python
import logging
logging.basicConfig(level=logging.DEBUG)
logging.getLogger('urllib3').setLevel(logging.DEBUG)
```

### 5.2 Diagnostic dump for bug reports

```bash
python -m requests.help
```

Output includes Python implementation/version, OS, urllib3, OpenSSL,
PyOpenSSL/cryptography, `idna`, `charset_normalizer` vs `chardet`, and the
Requests version. The maintainers ask for this on every bug ticket; embed
the call into your support tooling.

### 5.3 Response hooks for telemetry

The `response` hook fires once per response, including each redirect hop.
Wire it to your APM:

```python
def telemetry(r, *args, **kwargs):
    metrics.timing('http.elapsed', r.elapsed.total_seconds(),
                   tags=[f'host:{urlparse(r.url).hostname}',
                         f'status:{r.status_code}',
                         f'method:{r.request.method}'])

s.hooks['response'].append(telemetry)
```

Hooks must not raise — an exception in a hook propagates out of `Session.send()`
and aborts the request even if the network exchange succeeded.

> [!NOTE]
> If a hook returns a non-`None` value, that value replaces the response
> seen by everything downstream (subsequent hooks, redirect logic, the
> caller). Useful for response-rewriting, dangerous if accidental.

### 5.4 Health checks for outbound dependencies

Requests does not provide a health-check primitive. The conventional
pattern is a `HEAD` against the upstream's documented health endpoint, with
a tight timeout:

```python
def healthy(s, url, timeout=2.0):
    try:
        return 200 <= s.head(url, timeout=timeout, allow_redirects=False).status_code < 400
    except requests.RequestException:
        return False
```

### 5.5 Operational checklist

- [ ] Every external `requests.*` call has an explicit `timeout=`.
- [ ] Long-lived workers use a `Session`, not the top-level helpers.
- [ ] Session lifetime is bounded (closed in `try/finally` or `with`).
- [ ] No `verify=False` in production paths.
- [ ] Either `REQUESTS_CA_BUNDLE` is unset (uses `certifi`) or it points
      at a curated, monitored corporate bundle.
- [ ] If retries are enabled, `allowed_methods` excludes non-idempotent
      verbs unless idempotency keys are in use.
- [ ] `Retry-After` header is respected (`respect_retry_after_header=True`).
- [ ] Hooks log to telemetry but do not raise.
- [ ] No sessions created before `os.fork()` or pre-fork worker spawn.

## 6. Performance

### 6.1 Connection reuse is the dominant lever

A single TLS handshake to a typical HTTPS host costs 50-300 ms;
keep-alive connection reuse drops subsequent requests to single-digit ms
plus server processing. The default `pool_maxsize=10` keeps up to ten
keep-alive connections per host. Always reuse a `Session` for any
non-trivial workload.

Benchmark pattern (rough order of magnitude on a typical cloud VM hitting a
nearby HTTPS endpoint):

| Pattern | Per-request cost (approx.) |
|---|---|
| `requests.get()` per call (one-shot session) | TLS handshake every time — typical 50-300 ms. |
| `Session.get()` with default pool | Handshake once, then ~response-time + RTT. |
| `Session.get()` with `pool_maxsize=N` and N concurrent threads | Limited by upstream concurrency / your network. |

### 6.2 Encoding detection has cost

Calling `r.text` on a response with no `charset` in `Content-Type` triggers
`apparent_encoding`, which feeds the entire body through
`charset_normalizer.detect()` (or `chardet.detect()`). For bodies measured
in megabytes, this is significant. Prefer:

- `r.content` if you only need bytes.
- `r.json()` if the body is JSON (auto-detects UTF-{8,16,32} via BOM/null
  pattern in `utils.guess_json_utf()` — much cheaper).
- Manually setting `r.encoding = 'utf-8'` if you know the answer.

### 6.3 `prepare_body` allocates

`PreparedRequest.prepare_body()` materializes the entire body unless `data=`
is a stream/generator. For multi-megabyte uploads, pass a file-like object
or a generator and let urllib3 chunk the upload:

```python
with open('big.bin', 'rb') as f:
    requests.put(url, data=f)   # streamed; sets Content-Length via super_len
```

### 6.4 Cookie jar dict access is O(n)

`RequestsCookieJar` exposes a dict interface for compatibility but cookie
storage is a list internally. Operations docstrings warn that *"dictionary
operations that are normally O(1) may be O(n)"*. For sessions accumulating
hundreds of cookies, prefer iterating directly or using
`remove_cookie_by_name()` instead of `del jar[name]`.

### 6.5 Header validation is cheap but mandatory

`prepare_headers` calls `check_header_validity()` for each header. The two
regexes (`_VALID_HEADER_NAME_RE_STR`, `_VALID_HEADER_VALUE_RE_STR`) are
compiled once at module import; per-header cost is negligible. The check
exists to stop CRLF injection, and it is not optional — invalid headers
raise `InvalidHeader` rather than silently corrupting the wire.

## 7. Security

### 7.1 TLS verification model

By default, `verify=True`, and `requests.certs.where()` returns the
`certifi` bundle path. The bundle is shipped as a `certifi` package data
file and updated on each `pip install -U certifi`. The actual verification
happens in `urllib3` via OpenSSL; Requests configures it through
`HTTPAdapter.cert_verify()`:

```python
# adapters.py:cert_verify (paraphrased)
if url is HTTPS and verify is True:
    cert_loc = DEFAULT_CA_BUNDLE_PATH                # certifi
elif verify is a path:
    cert_loc = verify
if cert_loc and not os.path.exists(cert_loc):
    raise OSError("Could not find a suitable TLS CA certificate bundle, ...")
conn.cert_reqs = "CERT_REQUIRED"
conn.ca_certs  = cert_loc   (or conn.ca_cert_dir if a directory)
```

When `verify=False`, `cert_reqs="CERT_NONE"` and both `ca_certs` and
`ca_cert_dir` are unset — the connection accepts any presented certificate.

> [!CAUTION]
> Setting `verify=False` defeats TLS authentication. The session docstring
> explicitly says: *"requests will accept any TLS certificate presented by
> the server, and will ignore hostname mismatches and/or expired
> certificates, which will make your application vulnerable to
> man-in-the-middle (MitM) attacks."* This is appropriate for unit tests
> only — never production.

### 7.2 Client certificate (mTLS)

```python
verify = True                                 # still verify the server!
cert   = '/etc/ssl/client.pem'                # combined PEM
# or
cert   = ('/etc/ssl/client.crt', '/etc/ssl/client.key')

requests.get(url, verify=verify, cert=cert)
```

`HTTPAdapter.cert_verify()` validates that the cert and key paths exist and
raises `OSError` otherwise. Test certificate fixtures live in `tests/certs/`
(valid CA + server, expired CA + server, mTLS CA + client) and are
regenerated via `make` in those directories.

### 7.3 Redirect credential stripping

`SessionRedirectMixin.should_strip_auth()` removes the `Authorization`
header when:

- the redirect changes hostname, OR
- the redirect changes scheme, EXCEPT for the standard
  `http://...:80 → https://...:443` upgrade, OR
- the redirect changes port (other than the default-port case).

`Proxy-Authorization` is unconditionally removed on redirect (because the
new request may go through a different proxy). After stripping, Requests
re-applies any `.netrc` auth that matches the new host (when
`trust_env=True`). This is the intended behavior; it is also why **a custom
`Authorization` header set via `headers={'Authorization': ...}` may
disappear on redirect.** If you want a header that survives, register an
`AuthBase` instance instead — auth handlers re-run on the redirected
request.

### 7.4 URL validation and IDNA

`PreparedRequest.prepare_url()`:

- Decodes bytes URLs as UTF-8.
- Strips leading whitespace.
- For non-HTTP schemes (`mailto:`, `data:`), skips parsing and stores the
  URL verbatim.
- Calls `urllib3.util.parse_url()`; bad parses raise `InvalidURL`.
- Demands a scheme — missing schemes raise `MissingSchema` with a hint to
  prepend `https://`.
- Demands a host — empty hosts raise `InvalidURL("Invalid URL '{url}': No
  host supplied")`.
- For non-ASCII hostnames, runs `idna.encode(host, uts46=True)`. IDNA
  errors are translated to `InvalidURL("URL has an invalid label.")`.
- Rejects ASCII hosts that start with `*` or `.`.
- Re-percent-encodes the URL via `requote_uri()`.

This pipeline blocks several classes of URL smuggling: empty hosts,
wildcard hosts, malformed IDNA labels, and stray whitespace. There is no
explicit DNS rebind protection — the IP address is resolved by urllib3 at
connection time.

### 7.5 Header injection blocking

`utils.check_header_validity()` enforces:

- Header names match `^[^:\s][^:\r\n]*\Z`.
- Header values match `^\S[^\r\n]*\Z|^\Z` (no leading whitespace, no embedded
  CR/LF).

Either failure raises `InvalidHeader` *before* any bytes hit the wire. Header
parts must be `str` or `bytes`; other types raise `InvalidHeader` with the
message
`"Header part ({header_part!r}) from {header} must be of type str or bytes, not {type}"`.

This stops the classic CRLF-injection class of attacks.

### 7.6 Cookie security

`RequestsCookieJar` inherits from `cookielib.CookieJar` and uses its
default policy. That means standard browser cookie semantics — `Domain`,
`Path`, `Secure`, `HttpOnly`, expiry — are honored. Notably:

- Secure cookies are only sent over HTTPS.
- The redirect handler purges the `Cookie` header before re-emitting the
  request to ensure cookies are recomputed against the new URL via
  `prepare_cookies`.

### 7.7 Proxy security

The `Proxy-Authorization` header is **never** appended to HTTPS-tunneled
requests (those go through CONNECT and would leak the credential to the
upstream). The check is in `SessionRedirectMixin.rebuild_proxies()`:

```python
if not scheme.startswith("https") and username and password:
    headers["Proxy-Authorization"] = _basic_auth_str(username, password)
```

When the proxy URL itself contains credentials (`http://user:pass@proxy`),
Requests extracts them via `get_auth_from_url()` and passes them to the
appropriate proxy header — but only on plaintext schemes.

### 7.8 Secrets handling

Requests does not log credentials, and does not provide any redaction.
Application code is responsible for keeping `Authorization` headers out
of its own logs and out of error reports. A useful convention:

```python
def safe_repr(headers):
    h = headers.copy()
    if 'Authorization' in h:
        h['Authorization'] = '<redacted>'
    return dict(h)
```

> [!WARNING]
> The `__repr__()` of `Request`/`PreparedRequest`/`Response` **does not**
> include the headers or body — only the method or status code. That is
> safe by default. But `pickle.dumps(prepared)` *will* include all headers,
> credentials and all. Treat pickled requests as secrets.

### 7.9 Known security-adjacent open issues

Carry these as risks if relevant to your deployment:

- **#5316** — `HTTPAdapter` with a user-supplied `SSLContext` does not honor
  the context's `ca_certs` on Windows. Workaround: set `verify=` to the
  path of the CA bundle as well.
- **#7040** — 2.32.5 changed the way custom SSL contexts are passed; some
  callers that worked on 2.32.4 broke. Verify your custom context plumbing
  against your target version.
- **#6810** — `auth.py` uses MD5 indirectly via `hashlib.md5(..., usedforsecurity=False)`,
  which is FIPS-tolerant since Python 3.9; the issue tracks completing the
  FIPS hardening.
- **#7271** — type annotations are RFC'd but not yet inline; scanners
  expecting `py.typed` may show no inference.

## 8. Developer Internals

This section is the "if you fork or extend Requests, here is what you need
to know" reference.

### 8.1 Repository layout

```
requests/
├── src/requests/                 # all runtime code
│   ├── __init__.py               # public re-exports + dependency check
│   ├── __version__.py            # __version__ = '2.33.1', __build__, ...
│   ├── _internal_utils.py        # to_native_string, HEADER_VALIDATORS
│   ├── adapters.py               # BaseAdapter, HTTPAdapter, urllib3 bridge
│   ├── api.py                    # get/post/put/.../request top-level helpers
│   ├── auth.py                   # AuthBase, HTTPBasicAuth, HTTPDigestAuth, HTTPProxyAuth
│   ├── certs.py                  # where() — certifi bundle path
│   ├── compat.py                 # urllib3 v1/v2 + chardet/charset_normalizer
│   ├── cookies.py                # RequestsCookieJar, MockRequest, MockResponse
│   ├── exceptions.py             # full exception hierarchy
│   ├── help.py                   # python -m requests.help
│   ├── hooks.py                  # HOOKS = ['response'], default_hooks, dispatch_hook
│   ├── models.py                 # Request, PreparedRequest, Response
│   ├── packages.py               # backwards-compat re-exports
│   ├── sessions.py               # Session, SessionRedirectMixin, merge_setting
│   ├── status_codes.py           # codes LookupDict
│   ├── structures.py             # CaseInsensitiveDict, LookupDict
│   └── utils.py                  # default_headers, proxies, netrc, IDNA helpers
├── tests/                        # full test suite (test_requests.py, etc.)
│   ├── certs/                    # mTLS test fixtures (valid, expired, mtls)
│   └── testserver/               # local socket-server harness for integration tests
├── docs/                         # Sphinx user docs (api, install, quickstart, advanced)
├── ext/                          # logo and brand assets (SVG, PNG)
├── pyproject.toml                # PEP 517 build, deps, project metadata
├── requirements-dev.txt          # dev pins
├── setup.py                      # 7-line shim that enforces Python >= 3.10
├── HISTORY.md                    # changelog
├── README.md                     # banner README
├── LICENSE                       # Apache 2.0
└── tox.ini, Makefile, MANIFEST.in
```

### 8.2 Key abstractions (cheat-sheet)

| Class | Inherits | Responsibility |
|---|---|---|
| `BaseAdapter` | — | Abstract: `send()`, `close()`. |
| `HTTPAdapter` | `BaseAdapter` | The reference impl over urllib3; the only adapter ships in-tree. |
| `AuthBase` | — | Abstract auth handler; subclass to add auth schemes. |
| `HTTPBasicAuth` | `AuthBase` | Authorization: Basic. |
| `HTTPProxyAuth` | `HTTPBasicAuth` | Same but writes Proxy-Authorization. |
| `HTTPDigestAuth` | `AuthBase` | RFC 7616 digest, thread-local nonce state. |
| `RequestException` | `IOError` | Root of all Requests exceptions. |
| `RequestsWarning` | `Warning` | Root of all Requests warnings. |
| `RequestsCookieJar` | `cookielib.CookieJar`, `MutableMapping` | Pickleable cookie jar with dict interface. |
| `CaseInsensitiveDict` | `MutableMapping` | Header dict (preserves original key case, lookups case-insensitive). |
| `LookupDict` | `dict` | Attribute-access dict (`codes.ok`); missing keys return `None`. |
| `Request` | `RequestHooksMixin` | User-facing constructor; `prepare()` returns `PreparedRequest`. |
| `PreparedRequest` | `RequestEncodingMixin`, `RequestHooksMixin` | The wire-bytes-ready form of a request. |
| `Response` | — | Server response, with eager/lazy body access. |
| `Session` | `SessionRedirectMixin` | The connection/cookie/auth/proxy/hook context. |

The full inheritance graph (47 classes) lives in
`extracted/class_hierarchy.tsv`; reading it quickly orients you to the
exception tree, which is intentionally deep so callers can catch at any
granularity (`except Timeout` catches both connect and read timeouts;
`except ConnectionError` catches `ProxyError` and `SSLError`; `except
RequestException` catches everything).

### 8.3 The exception hierarchy

```
IOError
└── RequestException
    ├── HTTPError
    ├── ConnectionError
    │   ├── ProxyError
    │   ├── SSLError
    │   └── ConnectTimeout                 (also: Timeout)
    ├── Timeout
    │   ├── ConnectTimeout                 (also: ConnectionError)
    │   └── ReadTimeout
    ├── URLRequired
    ├── TooManyRedirects
    ├── MissingSchema                      (also: ValueError)
    ├── InvalidSchema                      (also: ValueError)
    ├── InvalidURL                         (also: ValueError)
    │   └── InvalidProxyURL
    ├── InvalidHeader                      (also: ValueError)
    ├── ChunkedEncodingError
    ├── ContentDecodingError               (also: urllib3.exceptions.HTTPError)
    ├── StreamConsumedError                (also: TypeError)
    ├── RetryError
    ├── UnrewindableBodyError
    ├── InvalidJSONError
    │   └── JSONDecodeError                (also: json.JSONDecodeError)

Warning
└── RequestsWarning
    ├── FileModeWarning                    (also: DeprecationWarning)
    └── RequestsDependencyWarning
```

`RequestException.__init__` accepts a `request=` and a `response=` kwarg so
exceptions know which request/response they were attached to —
`exc.request`, `exc.response`. If only `response` is passed and it has a
`request`, the request is auto-populated.

### 8.4 Subclassing `HTTPAdapter`

The full set of override hooks (all explicitly documented in the source as
"only exposed for use when subclassing"):

| Method | When/why to override |
|---|---|
| `init_poolmanager(connections, maxsize, block, **pool_kwargs)` | Pass extra kwargs into urllib3 PoolManager (e.g. an `ssl_context`). |
| `proxy_manager_for(proxy, **proxy_kwargs)` | Customize per-proxy ProxyManager construction. |
| `cert_verify(conn, url, verify, cert)` | Override the TLS verification configuration (e.g. force cert pinning). |
| `build_response(req, resp)` | Customize the `Response` returned (add custom attrs, etc.). |
| `build_connection_pool_key_attributes(request, verify, cert)` | Inject `ssl_context` or other PoolKey attributes. |
| `get_connection_with_tls_context(request, verify, proxies, cert)` | Replace the connection-acquisition path entirely. |
| `request_url(request, proxies)` | Customize the request-target (path vs absolute URL). |
| `add_headers(request, **kwargs)` | Inject extra headers at send time (no-op default). |
| `proxy_headers(proxy)` | Customize Proxy-Authorization assembly. |
| `send(request, stream, timeout, verify, cert, proxies)` | Replace the entire send pipeline. |

The deprecated `get_connection(url, proxies)` remains for source-compat
with adapters written for Requests <2.32.2. New subclasses should use
`get_connection_with_tls_context()`. The deprecation issues a
`DeprecationWarning` on every call.

Minimal example — pin a specific `SSLContext`:

```python
import ssl
from requests.adapters import HTTPAdapter

class PinnedAdapter(HTTPAdapter):
    def __init__(self, *args, ssl_context=None, **kw):
        self._ssl_context = ssl_context or ssl.create_default_context()
        super().__init__(*args, **kw)

    def init_poolmanager(self, connections, maxsize, block=False, **pool_kwargs):
        pool_kwargs['ssl_context'] = self._ssl_context
        super().init_poolmanager(connections, maxsize, block, **pool_kwargs)

ctx = ssl.create_default_context(cafile='/etc/corp/ca.pem')
ctx.set_ciphers('ECDHE+AESGCM')
s = requests.Session()
s.mount('https://', PinnedAdapter(ssl_context=ctx))
```

### 8.5 The hook system

Defined in `hooks.py`:

```python
HOOKS = ['response']

def default_hooks():
    return {event: [] for event in HOOKS}

def dispatch_hook(key, hooks, hook_data, **kwargs):
    hooks = hooks or {}
    hooks = hooks.get(key)
    if hooks:
        if hasattr(hooks, '__call__'):
            hooks = [hooks]
        for hook in hooks:
            _hook_data = hook(hook_data, **kwargs)
            if _hook_data is not None:
                hook_data = _hook_data
    return hook_data
```

There is one — and only one — event today: `response`. Registering hooks for
any other event raises `ValueError("Unsupported event specified, with event
name {event}")` at `register_hook()`. The kwargs passed to a hook are the
same ones used for the request (`stream`, `timeout`, `verify`, `cert`,
`proxies`). Hooks receive the kwargs so they can build follow-up requests
that match the original (this is exactly how `HTTPDigestAuth.handle_401`
works internally).

### 8.6 Redirect logic — `SessionRedirectMixin`

The mixin decomposes redirect handling into:

- `get_redirect_target(resp)` — returns the next URL or `None`. Re-encodes
  the `Location` header in latin-1 then re-decodes as utf-8 to handle
  servers that emit UTF-8 in headers (which is technically out-of-spec but
  common in practice).
- `should_strip_auth(old_url, new_url)` — see Security section above.
- `resolve_redirects(resp, req, ...)` — generator yielding either Responses
  or PreparedRequests (controlled by `yield_requests`). Handles:
  - Cumulative redirect history.
  - `TooManyRedirects` after `max_redirects`.
  - Scheme-less `Location` headers (`//host`).
  - Relative `Location` headers via `urljoin` + `requote_uri`.
  - Body removal for non-307/308 (`Content-Length`, `Content-Type`,
    `Transfer-Encoding` stripped on 301/302/303).
  - Cookie purge before re-prepare.
  - Proxy and auth rebuild via `rebuild_proxies` / `rebuild_auth`.
  - Body rewinding via `rewind_body()` for re-sending.
- `rebuild_method(prepared, response)` — implements the `301-after-POST →
  GET` browser quirk and the `302-after-anything-but-HEAD → GET` quirk.
  Per the source comment, *"Do what the browsers do, despite standards..."*.

### 8.7 Proxy resolution

`utils.resolve_proxies(request, proxies, trust_env)` is the canonical
proxy-decision function. Order of precedence:

1. The `proxies` kwarg passed to the call.
2. The Session's `proxies`.
3. Environment variables (`HTTP_PROXY`, `HTTPS_PROXY`, ...) if `trust_env`.

`should_bypass_proxies(url, no_proxy)` checks:

- Explicit `no_proxy` argument (or `NO_PROXY`/`no_proxy` env var if not
  passed).
- Hostname match against the comma-separated bypass list (suffix matches,
  CIDRs).
- Per-platform stdlib `proxy_bypass()` (consulted on Linux/macOS).
- On Windows, `proxy_bypass_registry()` parses `Internet Settings` registry
  entries directly to avoid DNS lookups (the stdlib `proxy_bypass` does
  reverse DNS, which is slow).

> [!NOTE]
> Open issue #2018 tracks a long-standing complaint about proxy precedence
> ordering. Treat the rules above as the current behavior; do not write
> code that depends on a particular precedence rule changing in the future.

### 8.8 Body encoding pipeline

`PreparedRequest.prepare_body(data, files, json)` decides among four shapes:

1. **JSON only** (`data` falsey, `json` not None) — `json.dumps(json,
   allow_nan=False)`, set `Content-Type: application/json`.
2. **Streaming** (`data` is iterable but not str/list/tuple/Mapping) —
   probe length via `super_len`. If known, set `Content-Length`; else set
   `Transfer-Encoding: chunked`.
3. **Multipart** (`files` is non-empty) — `_encode_files(files, data)`
   produces a multipart body and Content-Type via
   `urllib3.filepost.encode_multipart_formdata`.
4. **Form-urlencoded** (`data` is a Mapping) — `_encode_params(data)`
   produces `key=value&key=value`. The body bytes set `Content-Length`
   automatically.

Plain `bytes`/`str` `data` are passed through verbatim — Content-Type is
*not* set automatically; the caller must include it via `headers=`.

### 8.9 The `compat.py` shim

Everything Requests imports from `urllib3` or `urllib.parse` flows through
`compat.py`, which:

- Detects urllib3 1.x vs 2.x via `is_urllib3_1`.
- Resolves `chardet` vs `charset_normalizer` (preferred order: `chardet`
  first, then `charset_normalizer`).
- Re-exports `Mapping`, `MutableMapping`, `Callable` from `collections.abc`,
  `Morsel` from `http.cookies`, `cookielib` from `http.cookiejar`, and the
  legacy `urlparse`/`urlunparse`/`urljoin`/`urlencode`/`quote`/`unquote`
  from `urllib.parse`.
- Aliases `simplejson` → `json` if installed (so `complexjson.dumps` uses
  `simplejson` when available — it is faster on some workloads and
  understands a broader JSON syntax).
- Provides legacy aliases `basestring = (str, bytes)`, `bytes = bytes`,
  `str = str`, `integer_types = (int,)` for source compat with the
  Python 2 era.

### 8.10 Testing strategy

- `tests/test_requests.py` — the bulk of the integration tests, exercising
  every public API. Uses `pytest`, `pytest-mock`, and `responses`-style
  patterns.
- `tests/test_lowlevel.py` — wire-format and `prepare_*` unit tests.
- `tests/test_adapters.py` — `HTTPAdapter` behavior, retries, pool sizing,
  TLS configuration.
- `tests/test_hooks.py` — hook registration, dispatch ordering.
- `tests/test_utils.py` — proxy resolution, header validation, IDNA.
- `tests/test_structures.py` — `CaseInsensitiveDict`, `LookupDict`.
- `tests/testserver/server.py` — a tiny single-file `threading.Thread`-based
  HTTP server (`Server`, `TLSServer`) used for integration tests. It is
  intentionally minimal: it consumes the request bytes, returns a
  configurable response, and supports basic TLS and mutual-TLS via the
  `tests/certs/{valid,expired,mtls}/` certificate fixtures.

The cert fixtures are regenerated via `make` in `tests/certs/{valid,expired,mtls}/`
which runs `openssl req`, `openssl x509`, etc. Expired certs are issued with
`-days 0` so they are immediately invalid — used to test that
`SSLError` actually fires.

`.coveragerc` and `tox.ini` configure multi-Python test runs. CI uses ruff
(via `.pre-commit-config.yaml`: `ruff-check --fix`, `ruff-format`) for
linting; PRs must pass.

### 8.11 Code patterns and conventions

- **Single-source-of-truth dependency check.** Both `__init__.py:check_compatibility`
  and `compat.py` interrogate `urllib3.__version__`; the version table and
  warning categories live in `__init__.py`.
- **`__attrs__` for safe pickling.** `Session`, `Response`, and `HTTPAdapter`
  each declare an `__attrs__` list of pickleable attribute names; their
  `__getstate__`/`__setstate__` use that list as the contract.
- **`_internal_utils.py` is the dependency-free leaf.** Anything in
  `_internal_utils.py` must not import other Requests modules; the
  `cookies.py` docstring warns *"requests.utils imports from here, so be
  careful with imports"*.
- **Backward-compatibility re-exports.** `requests.packages` exists so old
  code doing `from requests.packages.urllib3 import X` continues to work.
  `requests.compat` re-exports `chardet` even when only
  `charset_normalizer` is installed.
- **Default hooks are *fresh dicts*.** `default_hooks()` returns a new
  `{event: []}` per call so two Requests don't share a hook list.
- **`merge_setting` skips `None`.** Setting a per-request header to `None`
  *removes* the session-level header for that one request — useful for
  one-shot UA suppression.
- **`mount()` sorts adapters by descending prefix length.** Longest match
  wins, which means `s.mount('https://api.example.com/', special_adapter)`
  is consulted before the generic `s.mount('https://', HTTPAdapter())`.

### 8.12 Potential improvements (open RFCs and known gaps)

These are upstream open items as of the briefing snapshot:

- **#7271** RFC: inline type annotations. Adding `py.typed` and full type
  hints across the public API.
- **#5498** Use `urllib3.HTTPHeaderDict` to avoid a redundant
  data-conversion when constructing request headers.
- **#7350** Clarify the `timeout=` parameter behavior in docs (especially
  the `(connect, read)` tuple semantics and `None` as no-timeout).
- **#7341** Documentation for `ChunkedEncodingError` is misleading (the
  doc says one thing, the code does another).
- **#2018** Re-order proxy precedence — long-standing dispute about
  whether session, env, or kwargs should win in edge cases.
- **#6830** `PreparedRequest` cannot bypass URL normalization when proxies
  are used.
- **#4664** `Session` does not handle servers that close keep-alive
  sockets cleanly — manifests as `ConnectionError` on the next request.
- **#6711** Some AWS S3 SDK paths broke after 2.32.0 changes — possible
  retry/header behavior regression.
- **#7188** DMTF Redfish fails with 2.32 — interaction between Redfish's
  custom auth and Requests' redirect handling.

Treat these as known issues; track in your dependency hygiene.

### 8.13 Contributing

Contributions follow the standard psf/requests workflow:

- Fork, branch, PR against `main`.
- Run `pre-commit` (`ruff-check --fix`, `ruff-format`) before pushing —
  pinned in `.pre-commit-config.yaml` to `ruff v0.15.9`,
  `pre-commit-hooks v6.0.0`.
- Tests live under `tests/`; new code without tests will not merge.
- The pinned Sphinx version (`Sphinx==7.2.6` in `docs/requirements.txt`)
  matches what RTD builds; bump it in lockstep with their config.
- Documentation source is in `docs/`; user-facing docs in `docs/user/`,
  community process in `docs/community/`, dev process in `docs/dev/`.

The project is licensed under **Apache 2.0**, copyright Kenneth Reitz —
the original author — with PSF maintaining since the transition.

## 9. Integration Points

### 9.1 What downstream code can reasonably depend on

These are the parts of Requests that are stable, public, and safe to import
from external code:

- `requests.{get,post,put,patch,delete,head,options,request}`
- `requests.{Session, Request, PreparedRequest, Response}`
- `requests.{ConnectionError, ConnectTimeout, FileModeWarning, HTTPError,
  JSONDecodeError, ReadTimeout, RequestException, Timeout, TooManyRedirects,
  URLRequired}`
- `requests.codes` (`LookupDict`)
- `requests.adapters.{BaseAdapter, HTTPAdapter}`
- `requests.auth.{AuthBase, HTTPBasicAuth, HTTPProxyAuth, HTTPDigestAuth}`
- `requests.cookies.{RequestsCookieJar, cookiejar_from_dict, extract_cookies_to_jar}`
- `requests.exceptions.*` (the full exception module)
- `requests.structures.{CaseInsensitiveDict, LookupDict}`
- `requests.utils.{default_headers, get_netrc_auth, dict_from_cookiejar, ...}`
- `requests.certs.where()`
- `requests.help.info()` for environment dumps

### 9.2 What is internal and may break

- `requests._internal_utils.*`
- The `_basic_auth_str` helper in `auth.py` (single underscore — private).
- `requests.compat.*` is a back-compat shim; new code should use stdlib
  imports directly (`from collections.abc import Mapping`, etc.).
- Methods documented as *"This method should not be called from user code,
  and is only exposed for use when subclassing"* — they are stable enough
  to subclass against, but treat the call surface as a subclass contract,
  not a public API.
- `HTTPAdapter.get_connection` (deprecated; use
  `get_connection_with_tls_context`).
- `requests.packages` — exists for legacy code; new code should import
  `urllib3` directly.

### 9.3 Common downstream integrators

Requests is the transport layer for thousands of clients; some patterns
worth knowing if you build on it:

- **Session-per-API-client.** Most SDKs (the AWS SDK's `botocore`, the
  Stripe SDK, the GitHub API SDKs) build one `Session` per client and
  attach a custom `HTTPAdapter` for retries and TLS.
- **Auth-handler libraries.** `requests-oauthlib`, `requests-aws4auth`, etc.
  subclass `AuthBase` to provide OAuth1/2 and SigV4 signing.
- **Caching layers.** `requests-cache` and `cachecontrol` mount custom
  adapters that intercept `send()` to serve from a cache.
- **Mocks.** `responses`, `requests-mock`, and `httpretty` patch Requests at
  the adapter or transport level for testing.

### 9.4 The deprecation surface

Watch for these `DeprecationWarning`s in your application logs:

| Warning trigger | Source | Notes |
|---|---|---|
| Non-string username/password to `HTTPBasicAuth` | `auth.py:_basic_auth_str` | Will be removed in Requests 3.0.0. |
| Calling deprecated `HTTPAdapter.get_connection()` | `adapters.py` | Use `get_connection_with_tls_context()`; required for Requests >=2.32.2 subclasses. |
| Calling `requests.session()` (lowercase) | `sessions.py:session()` | Deprecated since 1.0.0; use `Session()`. |
| Opening a text-mode file for upload | `utils.py:super_len` (raises `FileModeWarning`) | Open with `'rb'`. |
| Old `cryptography` (<1.3.4) | `__init__.py:_check_cryptography` | Slowness warning. |

Build pipelines should treat `DeprecationWarning` from Requests as a signal
to schedule a fix before the next major bump.

---

_Generated April 2026 from psf/requests at 514c162. AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it. Unofficial; not affiliated with or reviewed by the project._
