Пакет · sdk-s5w

Обработка ошибок

Всё, что бросает SDK, наследует S5wException, а класс исключения говорит, что случилось. Ветвитесь по классу; текст сообщения — для логов, он может меняться.

Три вида сбоев

Что случилось Исключение Повтор поможет?
сервер не ответил: нет сети, DNS, отказ соединения, таймаут TransportException да, позже
сервер ответил, но не как S5W: не JSON, не те поля ResponseException нет — неверный адрес или несовместимая версия
сервер отказал: ответ с кодом ошибки ApiException и его наследники зависит от кода, см. ниже

Полная иерархия и соответствие кодам — в справочнике Исключения.

Ключ не работает или сервер недоступен

Это разные ситуации, и реагировать на них нужно по-разному: мёртвый ключ надо заменить, а недоступный сервер — подождать.

php
use Flytachi\Winter\SDK\S5w\Exception\ForbiddenException;
use Flytachi\Winter\SDK\S5w\Exception\TransportException;
use Flytachi\Winter\SDK\S5w\Exception\UnauthorizedException;

try {
  $s5w->check();
} catch (UnauthorizedException) {
  // 401: сервер не знает такой ключ — ошибка в конфигурации
} catch (ForbiddenException) {
  // 403: ключ известен, но отключён или истёк — нужен новый, повтор не поможет
} catch (TransportException $e) {
  // сервер не ответил — можно повторить позже
  if ($e->isTimeout()) {
      // именно таймаут, а не отказ соединения
  }
}

Для проверок здоровья приложения есть короткая форма:

php
if (!$s5w->isValid()) {
  // ключ отклонён (401 или 403)
}

isValid() возвращает false только когда сервер отказал ключу. Если сервер недоступен, он по-прежнему бросает TransportException — «не удалось узнать» не то же самое, что «нет».

ForbiddenException бывает и с рабочим ключом: если ключ BASIC используется для folders(), files() или uploads(), которым нужен FULL.

Нет такого файла или папки

php
use Flytachi\Winter\SDK\S5w\Exception\NotFoundException;

try {
  $file = $s5w->files()->get($id);
} catch (NotFoundException) {
  return ResponseEntity::notFound();
}

Часто проще вообще не ловить исключение:

php
$file = $s5w->files()->find($id);    // null вместо исключения
$folder = $s5w->folders()->find('x');

Файл из чужого бакета выглядит точно так же, как несуществующий: сервер нарочно не различает эти случаи.

Несуществующая папка — это 422, а не 404

Если в upload(), list() или move() указана папка, которой нет, сервер отвечает 422: это ошибка в параметрах запроса, а не «ресурс не найден». Ловите ValidationException.

Ошибки валидации

ValidationException (422) приходит, когда параметры не прошли проверку. У него есть ошибки по полям — их удобно показать в форме:

php
use Flytachi\Winter\SDK\S5w\Exception\ValidationException;

try {
  $s5w->folders()->create($request->name);
} catch (ValidationException $e) {
  $e->errors();
  // ["name" => ["must not be blank", "size must be between 1 and 100", …]]

  $e->errorsOf('name');
  // ["must not be blank", "size must be between 1 and 100", …]
}

Бывает, что ошибка относится не к одному полю, а ко всему запросу — нет папки, опции картинки для не-картинки, хеш не совпал при завершении загрузки. Тогда errors() пустой, а суть — в сообщении:

text
S5W v1/files: 422 Folder "no-such" does not exist
S5W v1/files: 422 Image options were given, but this file is not an image

Имя уже занято

ConflictException (409) — при создании или переименовании папки с занятым именем и при переименовании или переносе файла, если в папке назначения есть файл с таким именем.

php
use Flytachi\Winter\SDK\S5w\Exception\ConflictException;

try {
  $s5w->files()->rename($file, 'report.pdf');
} catch (ConflictException) {
  $s5w->files()->rename($file, 'report-' . date('Ymd') . '.pdf');
}

Загрузка конфликта не даёт никогда: при совпадении имени сервер сам сохранит файл как report (1).pdf.

Не хватает места

PayloadTooLargeException (413) при загрузке означает, что квота бакета заполнена:

php
use Flytachi\Winter\SDK\S5w\Exception\PayloadTooLargeException;

try {
  $s5w->files()->upload($path, folder: 'backups');
} catch (PayloadTooLargeException) {
  $free = $s5w->check()->storage->free;
  $logger->error("Бакет заполнен, свободно {$free} байт");
}

Сбои загрузки частями

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

php
use Flytachi\Winter\SDK\S5w\Exception\UploadInterruptedException;
use Flytachi\Winter\SDK\S5w\Exception\UploadUnconfirmedException;

try {
  $file = $s5w->uploads()->send($path, folder: 'videos');
} catch (UploadInterruptedException $e) {
  // повторы кончились; сессия жива 24 часа — можно продолжить
  $file = $s5w->uploads()->resume($e->sessionId(), $path);
} catch (UploadUnconfirmedException $e) {
  // все байты отправлены и сборка запрошена, но ответ потерялся, а сессии уже нет:
  // файл, возможно, создан, но его id неизвестен
  $logger->error("Проверьте файл {$e->name()} вручную");
}
Исключение Что значит Что делать
UploadInterruptedException сеть или сервер так и не ответили после всех попыток resume($e->sessionId(), $path) позже
UploadUnconfirmedException ответ на сборку потерялся, сессии нет найти файл по имени в папке или загрузить заново, если дубликат допустим
любой другой ApiException сервер отклонил сами данные (квота, папка, размер части) исправить причину; SDK уже отменил сессию

Почему SDK не ищет файл сам в случае UploadUnconfirmedException: поиск по имени может найти чужой файл с таким же именем, а гадать опаснее, чем честно сообщить. Подробнее — в разделе Конвейер загрузки.

Поймать всё

php
use Flytachi\Winter\SDK\S5w\Exception\ApiException;
use Flytachi\Winter\SDK\S5w\S5wException;

try {
  $s5w->files()->upload($path);
} catch (ApiException $e) {
  $logger->warning($e->getMessage(), [
      'status' => $e->status(),   // 404, 409, 422…
      'method' => $e->method(),   // "POST"
      'path' => $e->path(),       // "v1/files"
      'errors' => $e->errors(),
  ]);
} catch (S5wException $e) {
  // сеть, неожиданный ответ, файл не читается с диска
  $logger->error($e->getMessage());
}

У всех исключений getPrevious() содержит исходную причину, если она была — например, исключение HTTP-клиента при обрыве связи.

Повторы: что повторять, а что нет

Исключение Повторять?
TransportException, ServerException (5xx) да, с паузой
UploadInterruptedException да, через resume()
UnauthorizedException, ForbiddenException нет — нужен другой ключ
ValidationException, ConflictException, NotFoundException нет — нужны другие параметры
PayloadTooLargeException нет, пока не освободится место
ResponseException нет — проверьте адрес сервера

Обычные вызовы можно повторять автоматически при сетевом сбое — параметр retries в S5wConfig. Учтите, что delete() не идемпотентен: если первая попытка удалила файл, а ответ потерялся, повтор получит 404. Загрузка одним запросом не повторяется никогда, даже с retries — повтор мог бы сохранить файл дважды.

См. также