# Прокси, потоки, диагностика и ошибки

Здесь описаны настройки для случаев, когда стандартного запроса недостаточно. Возможность конкретного сочетания зависит от выбранного профиля и транспорта; её проверяет открытая сессия.

<a id="routes"></a>
## Маршруты

В `HttpClient` и `AsyncHttpClient` аргумент `proxy=None` выбирает прямое соединение. Для прокси передайте URL или `ProxyRoute`. Если аргумент запроса пропущен, сохраняется настройка клиента. У `Client` и `AsyncClient` прокси задаётся только объектом `ProxyRoute` в конструкторе. Поддерживаемые схемы: `http`, `https`, `socks5`, `socks5h`, если их разрешают профиль и транспорт.

В URL допускаются закодированные логин и пароль. У `ProxyRoute(endpoint, username='', password='')` они передаются отдельно; `endpoint` не должен их содержать. `repr()` скрывает учётные данные. Путь должен быть пустым или `/`; query и fragment запрещены. Порты по умолчанию: 80, 443, 1080, 1080 соответственно; допустимый диапазон — 1–65535. Имя хоста прокси нормализуется через IDNA, адрес IPv6 заключается в скобки. Логин и пароль — строки до 1024 байт UTF-8 без управляющих символов. Функция `duckonet.http_client306.normalize_proxy(proxy)` возвращает нормализованный `ProxyRoute` или `None`.

`socks5` разрешает адрес целевого сайта локально, `socks5h` — через прокси. Для HTTPS-прокси требуется поддержка транспорта; его TLS-соединение проверяется отдельно от соединения с целевым сайтом. Неподдерживаемое сочетание вызывает ошибку. Если прокси не сработал, прямого подключения вместо него не будет. Для обычных URL-прокси `h3` и `auto` не поддерживаются. В 0.11.5 для `MasqueProxy` используется `protocol='h3'` (строгий H3), см. [MASQUE](masque.html). Переменные окружения с настройками прокси автоматически не используются.

`protocol` принимает `h1`, `h2`, `h3` или `auto`. Режимы `h3` и `auto` выбирают смешанный профиль: первое соединение может использовать HTTP/2, а затем перейти на HTTP/3 по Alt-Svc и доступности маршрута. Они не означают «только QUIC». Фактический протокол указан в `response.protocol`: 1, 2 или 3.

Сочетание варианта профиля и нормализованного прокси, включая учётные данные, определяет маршрут. Значения `h3` и `auto` у `protocol` выбирают один и тот же вариант, поэтому используют один маршрут. Клиент переиспользует его сессию. За время жизни клиента допускается до 8 маршрутов, включая начальный; превышение вызывает `ValueError`. Активная сессия не изменяется и не вытесняется. Маршруты используют общее ядро и cookies. Версию профиля, `profile_id` и доверенные CA нельзя переключить параметрами отдельного запроса.

<a id="capabilities"></a>
## HttpCapabilities и платформы

Получите `client.capabilities` после открытия клиента. Низкоуровневый эквивалент — `duckonet.http_native306.capabilities(session)`. Неизменяемая модель `HttpCapabilities` описывает открытую сессию:

| Поле | Значение |
|---|---|
| ordinary_http, cookie_management, shared_cookie_routes | bool — поддержка обычного HTTP, управления cookies и общих cookies маршрутов |
| diagnostics, preserve_url | bool — поддержка выбранной сессией |
| protocols | tuple[str] — доступные протоколы |
| conditional_proxy_schemes | tuple[str] — обычные схемы прокси; допустимость сочетания проверяется отдельно |
| websocket, http3_proxy | bool — наличие соответствующей возможности загруженного runtime |
| proxy_carriers | tuple[str] — MASQUE carriers; сейчас `h2`, если доступен HTTP3 proxy |
| unsupported | `streaming_upload`, `unbounded_body`; `http3_proxy` добавляется, если runtime не имеет этой возможности |
| timeout_max_seconds, redirects_max | 300 и 100 |
| request_max_bytes | 16777216 |
| response_default_bytes, response_max_bytes | 1048576 и 67108864 |
| immutable_routes_max | 8 |

Создание модели вручную не проверяет транспорт. Используйте значение, полученное у клиента.

Выпуск предназначен для CPython 3.12 x64. Для Windows действуют [требования к системным компонентам](install.html), для Linux — Ubuntu 24.04 x64. Windows 11 является системой с подтверждённой совместимостью, а не жёстким ограничением версии ОС в библиотеке. Профили выбираются из подписанного каталога с учётом прав. Firefox, Safari, macOS и ARM этим пакетом не поддерживаются.

<a id="streams"></a>
## HttpStream и AsyncHttpStream

`HttpStream` и `AsyncHttpStream` возвращаются из `client.stream()`. Открывайте их через `with` или `async with`, читайте тело до конца, затем вызывайте `result()`. Если нужен только файл или обработчик байтов, начните с [примеров скачивания](quickstart.html).

`history` содержит завершённые перенаправления, `response` — текущий `HttpResponse`. После открытия доступны код, протокол, URL и заголовки; `body` и `content` ещё равны `None`. Поля ответа делегируются `response`, остальные методы — базовому `Request` или `AsyncStream`. Обращаться к `_stream` и `_native` не нужно.

`read(size=65536)` возвращает байты. Пустой результат `b''` означает конец тела без ошибки чтения. Синхронная обёртка ждёт готовности и не возвращает промежуточный `None`, который возможен у низкоуровневого `Request.read()`. `iter_bytes(size=65536)`, обычная и асинхронная итерация читают до конца.

`result()` или `await result()` возвращает итоговый `HttpResponse`, не собирая заново прочитанное тело; `content` может оставаться `None`. Размер части — от 1 до 16777216 байт, по умолчанию 65536. У потока один читатель. Общий таймаут не начинается заново при следующей части или перенаправлении. `await AsyncHttpStream.cancel()` закрывает базовый поток; синхронный `cancel()` вызывает `Request.cancel()`. Даже после отмены завершите контекст: он отвечает за освобождение ресурсов.

<a id="session"></a>
## HTTP-запросы через Session и AsyncSession

`Session` предоставляет `compile`, `request`, `stream`, `consume` для запросов с заданным контекстом и методы с суффиксом `_http` для обычных HTTP-запросов с настройками cookies, диагностики и URL. На этом уровне приложение само управляет временем жизни объектов:

| Метод Session | Результат и владение ресурсами |
|---|---|
| compile_http(method, url, **kwargs) | Compiled; вызывающий код обязан вызвать release() |
| request_http(method, url, **kwargs) | Response; созданные Compiled и Request освобождаются автоматически |
| stream_http(method, url, **kwargs) | Request для чтения по частям; вызывающий код закрывает его контекст |
| consume_http(method, url, consumer, *, chunk_size=65536, **kwargs) | Response после передачи тела обработчику |

`request_http()` допускает только `body_mode=1`. `stream_http()` сам выбирает `body_mode=2` и запрещает явно задавать его. Лимит буфера `body_limit` у низкоуровневого `stream_http()` по умолчанию равен 65536 байт; у `HttpClient` — 1048576.

Параметры `compile_http()`: `headers=()` (пары), `body=b''`, `timeout_ms=30000` (целое 1–300000), `body_limit=1048576`, `header_limit=65536`, `body_mode=1`, `context=None`, `cookies=()`, `cookies_enabled=True`, `persist_cookies=True`, `body_present=False`, `preserve_url=False`, `diagnostics=False`, `force_h1=False`.

Несмотря на `context=None` в сигнатуре, для этого пути обязателен `BrowserHopContext`. Признак `body_present` должен соответствовать телу, логические флаги — иметь тип `bool`. `force_h1=True` принудительно выбирает HTTP/1.1. У `HttpClient` для этого используйте `protocol="h1"`. В `Session.compile()` такого аргумента нет.

На этом уровне не обрабатываются `params`, `json`, `data`, `files` и автоматические перенаправления; результат не оборачивается в `Headers` или `HttpResponse`. Cookies проходят проверку нативного ядра. Функция `duckonet.http_native306.compile_http(session, method, url, **options)` предоставляет тот же вызов.

`AsyncSession.request_http()`, `stream_http()` и `consume_http()` возвращают соответственно `Response`, `AsyncStream` и `Response`. Запрос и `consume_http()` вызываются с `await`, поток открывается через `async with`. Обработчик `consume_http()` может быть синхронным или возвращать объект для `await`; асинхронная сессия дождётся его. Публичного `AsyncSession.compile_http()` нет; отдельная компиляция доступна у `Session`.

`Session.cookies` и `AsyncSession.cookies` доступны после входа. `share_cookies_from(source)` подключает хранилище исходной сессии к ещё не использованной целевой сессии того же ядра и модуля. После компиляции или между разными ядрами операция запрещена. Для асинхронного варианта обе сессии должны быть открыты; сам метод остаётся синхронным.

<a id="wait-headers"></a>
### Ожидание заголовков

`Request.wait_headers(timeout=None)` и `await AsyncStream.wait_headers(timeout=None)` возвращают `HeaderSnapshot` окончательных заголовков без чтения тела. При истечении ожидания возвращается `None`; запрос при этом не отменяется. `timeout` — конечное неотрицательное число секунд; `None` означает ожидание готовности или завершения запроса.

`HeaderSnapshot` содержит `request_id`, `owner_id`, `epoch` (int) и `blocks` (кортеж `ResponseBlock`). Финальный блок имеет `section=2`, целочисленные `status_code` и `protocol`, заголовки — пары `bytes`. Раннее завершение с ошибкой сохраняет исходное исключение и частичный ответ.

Во время ожидания заголовков нельзя одновременно читать тот же поток. Истечение `timeout` оставляет запрос активным; отмена асинхронной задачи ожидания, напротив, отменяет запрос и освобождает его ресурсы.

<a id="compile-signature"></a>
### Сигнатура compile_http

Сигнатура низкоуровневого вызова (Session.compile_http принимает те же именованные аргументы):

```python
# Справочная сигнатура; не сетевой вызов.
def compile_http(session, method, url, *, headers=(), body=b'', timeout_ms=30000,
                 body_limit=1048576, header_limit=65536, body_mode=1, context=None,
                 cookies=(), cookies_enabled=True, persist_cookies=True,
                 body_present=False, preserve_url=False, diagnostics=False, force_h1=False):
    ...
```

`force_h1` относится к низкоуровневому вызову. У `HttpClient.request()` и в словаре `advanced` такого параметра нет: используйте `protocol='h1'`. Классы `Cookie` и `Cookies` импортируются из `duckonet`; управление хранилищем описано в разделе [Cookies](cookies.html).

`Request.headers_snapshot()` немедленно возвращает доступный `HeaderSnapshot` или `None`, если снимок ещё не готов. Снимок может не содержать окончательных заголовков. Чтобы дождаться их, используйте `wait_headers()`; окончательный блок имеет `section=2`.

Конструктор `Cookies(session)` предназначен для прямой работы с сессией. У клиента готовое хранилище доступно как `client.cookies`.

<a id="diagnostics"></a>
## Диагностика запроса

Диагностика по умолчанию отключена. При `diagnostics=True` и поддержке транспорта `response.diagnostics` содержит `status:int`, `timings:dict`, `valid_mask:int`, `not_applicable_mask:int`, `flags:int`, `outgoing_headers` (кортеж пар строк), `headers_redacted=True`, `header_representation='observed-logical-fields'`, `reused:bool|None`. Если наблюдение недоступно, возвращается `None`; `response.timings` также может быть `None`. Для перенаправлений смотрите `history[i].diagnostics`.

Время указано в секундах от начала передачи. Это накопленные отметки, а не отдельные длительности этапов. Значение `None` означает, что измерение недоступно.

| Ключ | Что измеряет |
|---|---|
| namelookup | Завершение разрешения имени. |
| connect | Завершение подключения к серверу или прокси. |
| appconnect | Завершение TLS-рукопожатия, когда оно применимо. |
| pretransfer | Готовность к передаче после подготовки соединения. |
| starttransfer | Начало получения ответа. |
| total | Время передачи до завершения. |

Биты 0–5 в `valid_mask` соответствуют перечисленным ключам. Без нужного бита значение равно `None`. `not_applicable_mask` отмечает неприменимые этапы, например при повторном использовании соединения. В `flags` бит 0 означает, что факт переиспользования известен, бит 1 — что соединение переиспользовано. Без бита 0 `reused=None`. Отсутствующее наблюдение нельзя подменять нулём или настройкой клиента.

`outgoing_headers` содержит наблюдаемые логические заголовки с замаскированными секретами. Это не текст HTTP/1 и не точная упаковка кадров HTTP/2 или HTTP/3. Если `status!=0`, поля могут быть пусты; настроенные заголовки не подставляются вместо отсутствующего наблюдения. Публичный режим выдачи незамаскированных данных не предусмотрен.

Для низкоуровневого ответа существует `duckonet.http_native306.copy_diagnostics(module, response)`: преобразует диагностику в словарь или возвращает `None`, если наблюдение недоступно. При работе через `HttpClient` используйте `response.diagnostics`.

<a id="errors"></a>
## Ошибки

Сначала различайте ответ сайта и сбой выполнения. HTTP 4xx/5xx — ответ сервера; `raise_for_status()` превращает его в `HTTPStatusError`. Неверные аргументы, невозможность подключиться и ошибка запуска клиента — другие исключения.

После определения категории:

- `validation` — исправьте аргументы запроса.
- `capability` — выберите поддерживаемое сочетание профиля, протокола и прокси.
- `limit` — уменьшите данные или увеличьте соответствующий лимит в допустимом диапазоне.
- `dns`, `tls`, `proxy` — проверьте адрес, доверенные сертификаты или параметры прокси соответственно.
- `http-status` — обработайте код и тело ответа целевого сайта.
- `startup`, `license` — проверьте код запуска и права API-ключа.

Не повторяйте автоматически запрос с изменением данных, если результат неизвестен: сервер мог успеть его выполнить.


`describe_http_error(error:BaseException)` возвращает неизменяемый `HttpErrorDetails`: `group:str`, `code:int|str|None`, `domain:int|None`, `native_code:int|None`, `phase:int|None`, `partial_response_available:bool`. Последние числовые поля по умолчанию равны `None`, признак частичного ответа — `False`. Исходное исключение и способ его перехвата не меняются; текст ошибки не копируется. Для `NativeError` сохраняются его коды, а сам частичный ответ остаётся в `error.partial`.

Категории ошибок:

| group | Условие |
|---|---|
| cancel | CancelledError или нативный код −5 |
| timeout | TimeoutError или −6 |
| validation | ValueError, TypeError или −1 |
| capability | −2: неподдерживаемая возможность |
| http-policy | −17, −18 или HTTP_REDIRECT_* |
| limit | −11: технический предел |
| dns, tls, http, lifecycle | Соответствующая нативная область ошибки |
| proxy | Только domain=9 с native_code=5 или 97 |
| transport | Общая ошибка транспорта |
| http-status | HTTPStatusError |
| startup, license | StartupError/CatalogError; license для кода с LICENSE, ENTITLEMENT, PERMISSION или API_KEY |
| unknown | Остальные случаи |

`HttpRedirectError(RuntimeError)` создаётся с `(code, response)` и сохраняет эти поля. `HttpTimeoutError(TimeoutError)` принимает `(message, partial=None)`, содержит код `HTTP_TOTAL_TIMEOUT`, поля `partial` и `partial_response_available`. Частичный результат может быть последним доступным `HttpResponse`.

`HTTPStatusError(RuntimeError)` принимает `response` и сохраняет его вместе со `status_code`; возникает при `raise_for_status()`. `NativeError` сохраняет `request_id`, `owner_id`, `epoch`, `operation` и `partial`. Проверяйте `outcome` и `body_complete`: частичный ответ не равен успешно завершённому. Ошибки Unicode и JSON не являются ошибками транспорта.

## Явный контекст запроса

Пример задаёт контекст cookies самостоятельно. Здесь `client` — уже открытый `HttpClient`:

```python
from dataclasses import replace
from duckonet import BrowserHopContext, CookieContext

url = 'https://example.com/api/items'
base = client.distribution.navigation_context(url)
browser = replace(base, cookies=CookieContext(url, url, kind='fetch', credentials='include'),
                  document_url=url, initial_navigation=False, user_activation=False, storage_access=False)
hop = BrowserHopContext(browser=browser, mode='cors', body_present=False)
response = client.get(url, context=hop, advanced={'timeout_ms': 45000})
```

В примере `client` уже открыт. Явный контекст запроса сам по себе не запускает браузерный Fetch. Не передавайте одновременно `advanced.timeout_ms` и `timeout_seconds` в одном вызове.



## Распаковка ответа

Тело gzip-ответа распаковывается при получении. В `response.content` и частях потока находятся уже распакованные байты; повторно вызывать `gzip.decompress()` не нужно. Заголовки могут сохранять `Content-Encoding` и исходный `Content-Length`, поэтому размер `content` может отличаться от заголовка.

При чтении целого ответа `body_limit` ограничивает распакованное тело. При потоковом чтении этот параметр ограничивает буфер, а не общий размер скачивания.

## Отмена запроса

Вход в `with client.stream(...)` завершается после получения заголовков конечного ответа. До этого отдельного объекта потока для отмены нет. Синхронную операцию можно прервать из другого потока вызовом `client.close()`: он закрывает весь клиент и отменяет все его активные запросы. После этого создайте новый экземпляр клиента.

В асинхронном коде отменяйте задачу запроса через `task.cancel()`. У уже открытого потока можно вызвать `await stream.cancel()`. В обоих случаях дождитесь выхода из контекста. Блокирующий пользовательский `file.read()` или обработчик `consume()` нельзя прервать посередине выполнения.

## Конструкторы потоков

Для интеграции с низкоуровневым запросом классы `HttpStream` и `AsyncHttpStream` доступны в `duckonet.http_client306`. Сигнатура: `(stream, encoding=None, *, history=(), deadline=None, response=None)`. `deadline` — абсолютное значение монотонных часов в секундах, а не длительность ожидания. При обычном использовании `client.stream()` создаёт эти объекты самостоятельно; вручную задавать `deadline` не нужно.
