Package · sdk-s5w

Error handling

Everything the SDK throws extends S5wException, and the exception’s class says what happened. Branch on the class; the message text is for logs and may change.

Three kinds of failure

What happened Exception Will a retry help?
the server did not answer: no network, DNS, refused connection, timeout TransportException yes, later
the server answered, but not like S5W: not JSON, wrong fields ResponseException no — wrong address or an incompatible version
the server refused: an error status ApiException and its subclasses depends on the status, see below

The full hierarchy and the status mapping — in the Exceptions reference.

The key doesn’t work, or the server is down

These are different situations, and they call for different reactions: a dead key has to be replaced, an unreachable server — waited for.

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: the server does not know this key — a configuration mistake
} catch (ForbiddenException) {
  // 403: the key is known but disabled or expired — a new one is needed, a retry won't help
} catch (TransportException $e) {
  // the server did not answer — can be retried later
  if ($e->isTimeout()) {
      // specifically a timeout, not a refused connection
  }
}

For application health checks there is a short form:

php
if (!$s5w->isValid()) {
  // the key was refused (401 or 403)
}

isValid() returns false only when the server refused the key. If the server is unreachable it still throws TransportException — “couldn’t find out” is not “no”.

ForbiddenException also happens with a working key: when a BASIC key is used for folders(), files() or uploads(), which need FULL.

No such file or folder

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

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

Often it is simpler not to catch at all:

php
$file = $s5w->files()->find($id);    // null instead of an exception
$folder = $s5w->folders()->find('x');

A file from someone else’s bucket looks exactly like a missing one: the server does not tell the two apart on purpose.

A missing folder is 422, not 404

If upload(), list() or move() names a folder that does not exist, the server answers 422: it is a mistake in the request’s parameters, not “resource not found”. Catch ValidationException.

Validation errors

ValidationException (422) comes when parameters fail validation. It carries per-field errors — handy to show next to a form:

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", …]
}

Sometimes the error is about the whole request rather than one field — no such folder, image options for a non-image, a hash mismatch when completing an upload. Then errors() is empty and the cause is in the message:

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

The name is taken

ConflictException (409) — when creating or renaming a folder with a taken name, and when renaming or moving a file into a folder that already has a file with that name.

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');
}

An upload never conflicts: on a name clash the server stores the file as report (1).pdf itself.

Not enough space

PayloadTooLargeException (413) on an upload means the bucket’s quota is full:

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

try {
  $s5w->files()->upload($path, folder: 'backups');
} catch (PayloadTooLargeException) {
  $free = $s5w->check()->storage->free;
  $logger->error("The bucket is full, {$free} bytes free");
}

Chunked-upload failures

A chunked upload survives short failures by itself: it retries a chunk up to three times, each time asking the server how much it got. Only two special exceptions come out:

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) {
  // the retries ran out; the session lives 24 hours — it can be continued
  $file = $s5w->uploads()->resume($e->sessionId(), $path);
} catch (UploadUnconfirmedException $e) {
  // every byte was sent and assembly requested, but the answer was lost and the session is gone:
  // the file may have been created, but its id is unknown
  $logger->error("Check the file {$e->name()} by hand");
}
Exception Meaning What to do
UploadInterruptedException the network or the server never answered after every attempt resume($e->sessionId(), $path) later
UploadUnconfirmedException the answer to assembly was lost, the session is gone find the file by name in the folder, or upload again if a duplicate is fine
any other ApiException the server refused the data itself (quota, folder, chunk size) fix the cause; the SDK has already cancelled the session

Why the SDK does not look the file up itself on UploadUnconfirmedException: a name search could find someone else’s file with the same name, and guessing is worse than saying so honestly. More in Upload pipeline.

Catch everything

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) {
  // network, an unexpected answer, a file that can't be read from disk
  $logger->error($e->getMessage());
}

On every exception getPrevious() holds the original cause, if there was one — the HTTP client’s exception on a broken connection, for example.

Retries: what to retry and what not

Exception Retry?
TransportException, ServerException (5xx) yes, after a pause
UploadInterruptedException yes, through resume()
UnauthorizedException, ForbiddenException no — another key is needed
ValidationException, ConflictException, NotFoundException no — other parameters are needed
PayloadTooLargeException no, until space is freed
ResponseException no — check the server address

Ordinary calls can be retried automatically on a network failure — the retries parameter of S5wConfig. Mind that delete() is not idempotent: if the first attempt deleted the file and the answer was lost, the retry gets 404. A single-request upload is never retried, even with retries — a retry could store the file twice.

See also