Пакет · sdk-s5w

HTTP и логирование

Весь обмен с сервером проходит через один внутренний класс — Http\HttpClient. Ресурсы folders(), files() и uploads() только собирают путь и тело запроса; cURL, статусы и разбор ответа они не видят. Здесь — что этот класс делает с каждым запросом.

Путь запроса

text
ресурс (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 заменяет заголовок целиком — например, чтобы в логах сервера было видно ваше приложение:

php
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, который проверяет наличие и тип каждого поля. Ошибка называет путь к полю:

text
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:

text
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), а контекст передаёт дальше без изменений:

text
[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 к своему клиенту:

php
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 — это нужно, чтобы отправлять запросы. Не сериализуйте конфигурацию и не отдавайте её наружу.