HTTP и логирование
Весь обмен с сервером проходит через один внутренний класс — Http\HttpClient. Ресурсы
folders(), files() и uploads() только собирают путь и тело запроса; cURL, статусы и
разбор ответа они не видят. Здесь — что этот класс делает с каждым запросом.
Путь запроса
ресурс (Files::get и т.п.)
│ метод, путь, query, тело
▼
HttpClient::request() адрес, заголовки, таймауты, повторы
▼
winter-cast → cURL сам запрос
▼
HttpClient::send() одна строка в лог; не 2xx → ApiException; нет ответа → TransportException
▼
HttpClient::decode() JSON → Payload; не JSON → ResponseException
▼
Entity::fromPayload() типизированные поля; поле не то → ResponseException
▼
File, Folder, Page …Заголовки
Каждый запрос получает:
| Заголовок | Значение |
|---|---|
Authorization |
Bearer {ключ} |
Accept |
application/json |
User-Agent |
WinterSDK/{версия} или значение S5wConfig::$userAgent |
Content-Type |
application/json для запросов с телом; multipart/form-data для загрузки одним запросом; application/octet-stream для частей |
Upload-Offset |
только у частей загрузки — смещение, с которого начинается часть |
User-Agent
По умолчанию — WinterSDK/{версия}. Версия берётся из Composer
(InstalledVersions::getPrettyVersion('flytachi/winter-sdk-s5w')) и вычисляется один раз на
процесс:
| Как установлен пакет | Заголовок |
|---|---|
с тега, например v1.2.0 |
WinterSDK/1.2.0 |
| через path-репозиторий | WinterSDK/dev-main |
| Composer не знает пакет | WinterSDK/dev |
S5wConfig::$userAgent заменяет заголовок целиком — например, чтобы в логах сервера было
видно ваше приложение:
new S5wConfig($url, $key, userAgent: 'bravo-mobile/2.3'); // User-Agent: bravo-mobile/2.3Таймауты
| Таймаут | По умолчанию | На какие запросы |
|---|---|---|
connectTimeout |
5 с | установка соединения — для всех запросов |
timeout |
15 с | обычные запросы: проверка ключа, папки, список и чтение файлов, переименование, удаление, открытие и статус сессии |
transferTimeout |
300 с | запросы с байтами файла: загрузка одним запросом, отправка части, сборка файла |
Таймауты задаются на каждый запрос из конфигурации — в том числе если вы передали свой
CastClient со своими значениями по умолчанию.
Повторы
Что повторяется при сетевом сбое (нет соединения, таймаут) — и сколько раз:
| Запрос | Повторы | Почему |
|---|---|---|
| обычные запросы API | S5wConfig::$retries раз (по умолчанию 1 — без повторов) |
паузы 500 мс, 1 с, 2 с… ±30 % (winter-cast) |
| загрузка одним запросом | никогда | повтор после потерянного ответа сохранил бы файл дважды |
| открытие сессии с хешем | никогда | по той же причине: оно может само создать файл |
| часть и сборка файла | до Uploads::ATTEMPTS = 3 |
свой механизм с вопросом к серверу, см. Конвейер загрузки |
Повторяются только сетевые сбои. Ответ сервера с ошибкой (4xx, 5xx) на обычных запросах не повторяется никогда — он сразу становится исключением.
retries и удаление
DELETE не идемпотентен на сервере. Если первая попытка удалила файл, а ответ потерялся,
повтор получит 404, и delete() бросит NotFoundException. Включая retries, считайте 404 на
удалении успехом.
Адрес и параметры
Адрес. baseUrl + '/' + путь. У baseUrl убираются пробелы и завершающий /.
Части пути кодируются. Имя папки (v1/folders/{name}) проходит через rawurlencode():
пробелы, кириллица и спецсимволы безопасны. id файла перед этим проверяется по формату
(File::ID_PATTERN) — строка не того вида не уходит на сервер вовсе, а сразу даёт
NotFoundException. Поэтому подставить в путь ../ или ? через id невозможно.
Параметры запроса. Значения null не отправляются — это «не задано». Пустая строка
отправляется как есть: так Folder::ROOT превращается в folder= (только корень). Чтобы
пустой search: '' не ушёл на сервер, files()->list() сам превращает его в null.
Ошибки и проверка ответов
Ответ 2xx — успех. Тело разбирается как JSON; пустое тело (204) — пустой результат.
Ответ не 2xx. Из тела берутся message и errors; если тела нет или это не JSON — текст
HTTP {код}. По статусу выбирается класс исключения (ApiException::from()), в него
записываются статус, метод, путь и ошибки по полям. Сообщение — S5W {путь}: {статус} {текст},
путь раскодирован.
Нет ответа — TransportException с исходным исключением в getPrevious().
Ответ не того вида. Тело не JSON или не объект/массив — ResponseException. Дальше
сущности читают поля через внутренний Support\Payload, который проверяет наличие и тип
каждого поля. Ошибка называет путь к полю:
S5W v1/check: bytes.quota must be int, got string
S5W v1/files: data[3].content.hash is missingТак несовместимость версий или прокси, который отвечает своей страницей, видны сразу и
понятно, а не как TypeError в глубине кода.
Время. Сначала разбирается RFC 3339 (2026-11-08T17:10:44+00:00) — так S5W отдаёт время
везде; потом формат с пробелом, который отдавали старые серверы (2026-11-08 17:10:44 +00:00);
всё остальное — в том числе сырой 2026-11-08 17:10:44+00 из PostgreSQL и доли секунды — общим
разборщиком.
Логирование
Логгер передаётся в S5w и получает два вида записей.
Строка на каждый запрос — уровень debug:
GET v1/check → 200 (21 ms)
POST v1/folders → 409 (28 ms)
GET v1/folders/нет такой → 404 (18 ms)
PATCH v1/uploads/16ffb6ab-… → 200 (890 ms)
GET v1/check → failed (0 ms): Connection failed: …Метод, раскодированный путь, статус и время; при сетевом сбое — failed и причина. Ошибки по
статусу пишутся тем же уровнем debug, а не warning: SDK их всё равно бросает исключением,
и решать, логировать ли ошибку, — вызывающему коду. Иначе одна ошибка попадала бы в лог
дважды.
События загрузки частями — debug, warning для повторов, error для окончательных
сбоев. Полный список — в Конвейере загрузки.
Только здесь SDK пишет warning и error сам: повторы он обрабатывает внутри и наружу не
бросает, так что без лога о них никто бы не узнал.
Сообщения не начинаются с S5W — канал логгера и так говорит, откуда строка. В исключениях
префикс S5W есть: исключение может всплыть далеко от SDK.
Подстановка значений
Сообщения написаны в стиле PSR-3, с плейсхолдерами: upload {id} started: {name}, …. Но PSR-3
лишь разрешает логгеру подставлять значения из контекста, а не обязывает: winter-logger и
Monolog без PsrLogMessageProcessor выведут {id} буквально. Поэтому SDK подставляет значения
сам, до передачи логгеру (внутренний Support\InterpolatingLogger), а контекст передаёт
дальше без изменений:
[DEBUG] (S5W): upload b3b53a23-… started: backup.sql, 100725660 bytes in chunks of 8388608
{"id":"b3b53a23-…","name":"backup.sql","size":100725660,"chunk":8388608}Строка читается с любым логгером, а логгеры, которые индексируют поля (ELK и т.п.), получают их отдельно. Логгер, который сам умеет подставлять значения, ничего не сломает: плейсхолдеров в сообщении уже нет.
Подробная трассировка HTTP
Если нужны и заголовки, и тела, подключите middleware winter-cast к своему клиенту:
use Flytachi\Winter\Cast\Common\CastClient;
use Flytachi\Winter\Cast\Stereotype\LoggingMiddleware;
$client = (new CastClient())->addMiddleware(new LoggingMiddleware($logger));
$s5w = new S5w($config, client: $client, logger: $logger);Встроенные строки SDK при этом сохраняются. Учтите, что подробная трассировка может записать то, что SDK намеренно не пишет.
Ключ
Ключ — секрет, и SDK следит, чтобы он не утекал:
- в логи не попадает — заголовки не записываются ни в строках запросов, ни в событиях;
- в отладочный вывод не попадает целиком —
var_dump($config)иprint_r($config)показываютs5w_…aaaa(__debugInfo()); - в исключениях его нет — в сообщении только путь и ответ сервера.
Сам объект S5wConfig хранит ключ в открытом свойстве apiKey — это нужно, чтобы отправлять
запросы. Не сериализуйте конфигурацию и не отдавайте её наружу.