Package · sdk-s5w

HTTP and logging

All traffic with the server goes through one internal class, Http\HttpClient. The folders(), files() and uploads() resources only build the path and the body; they never see cURL, status codes or answer parsing. Here is what that class does with every request.

Request path

text
resource (Files::get etc.)
  │ method, path, query, body
  ▼
HttpClient::request()      address, headers, timeouts, retries
  ▼
winter-cast → cURL         the request itself
  ▼
HttpClient::send()         one log line; not 2xx → ApiException; no answer → TransportException
  ▼
HttpClient::decode()       JSON → Payload; not JSON → ResponseException
  ▼
Entity::fromPayload()      typed fields; a wrong field → ResponseException
  ▼
File, Folder, Page …

Headers

Every request gets:

Header Value
Authorization Bearer {key}
Accept application/json
User-Agent WinterSDK/{version} or the value of S5wConfig::$userAgent
Content-Type application/json for requests with a body; multipart/form-data for a single-request upload; application/octet-stream for chunks
Upload-Offset only on upload chunks — the offset the chunk starts at

User-Agent

By default — WinterSDK/{version}. The version comes from Composer (InstalledVersions::getPrettyVersion('flytachi/winter-sdk-s5w')) and is computed once per process:

How the package is installed Header
from a tag, e.g. v1.2.0 WinterSDK/1.2.0
through a path repository WinterSDK/dev-main
Composer does not know the package WinterSDK/dev

S5wConfig::$userAgent replaces the header entirely — e.g. so the server’s logs show your application:

php
new S5wConfig($url, $key, userAgent: 'bravo-mobile/2.3');   // User-Agent: bravo-mobile/2.3

Timeouts

Timeout Default Which requests
connectTimeout 5 s establishing the connection — every request
timeout 15 s ordinary requests: the key check, folders, listing and reading files, rename, delete, opening a session and its status
transferTimeout 300 s requests carrying file bytes: a single-request upload, sending a chunk, assembling the file

The timeouts are set on every request from the configuration — including when you pass your own CastClient with its own defaults.

Retries

What is retried on a network failure (no connection, a timeout) — and how many times:

Request Retries Why
ordinary API requests S5wConfig::$retries times (default 1 — no retries) pauses of 500 ms, 1 s, 2 s… ±30 % (winter-cast)
a single-request upload never a retry after a lost answer would store the file twice
opening a session with a hash never for the same reason: it can create the file itself
a chunk and assembling the file up to Uploads::ATTEMPTS = 3 a mechanism of its own that asks the server first, see Upload pipeline

Only network failures are retried. A server error answer (4xx, 5xx) on ordinary requests is never retried — it becomes an exception at once.

retries and delete

DELETE is not idempotent on the server. If the first attempt deleted the file and the answer was lost, the retry gets 404 and delete() throws NotFoundException. With retries on, treat a 404 on delete as success.

Address and parameters

The address. baseUrl + '/' + path. baseUrl has spaces and a trailing / removed.

Path segments are encoded. A folder name (v1/folders/{name}) goes through rawurlencode(): spaces, non-Latin letters and special characters are safe. A file id is checked against its format first (File::ID_PATTERN) — a string of the wrong shape never reaches the server and gives NotFoundException at once. So ../ or ? cannot be slipped into a path through an id.

Query parameters. null values are not sent — that is “not given”. An empty string is sent as it is: that is how Folder::ROOT becomes folder= (the root only). To keep an empty search: '' from reaching the server, files()->list() turns it into null itself.

Errors and answer checks

A 2xx answer is success. The body is parsed as JSON; an empty body (204) is an empty result.

A non-2xx answer. message and errors are taken from the body; if there is no body or it is not JSON — the text HTTP {code}. The exception class is chosen by status (ApiException::from()), and the status, method, path and per-field errors are recorded in it. The message is S5W {path}: {status} {text}, the path decoded.

No answer — TransportException, with the original exception in getPrevious().

An answer of the wrong shape. A body that is not JSON or not an object/array — ResponseException. Then the entities read their fields through the internal Support\Payload, which checks every field’s presence and type. The error names the path to the field:

text
S5W v1/check: bytes.quota must be int, got string
S5W v1/files: data[3].content.hash is missing

So a version mismatch, or a proxy answering with its own page, shows up at once and clearly — not as a TypeError deep in the code.

Timestamps. RFC 3339 (2026-11-08T17:10:44+00:00), which S5W sends everywhere, is parsed first; then the space-separated form older servers used (2026-11-08 17:10:44 +00:00); anything else — including PostgreSQL’s raw 2026-11-08 17:10:44+00 and fractional seconds — through the general parser.

Logging

The logger is passed to S5w and gets two kinds of records.

One line per request — at 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: …

The method, the decoded path, the status and the time; on a network failure — failed and the cause. Status errors are logged at the same debug level, not warning: the SDK throws them as exceptions anyway, and whether to log an error is the calling code’s decision. Otherwise one error would end up in the log twice.

Chunked-upload events — debug, warning for retries, error for final failures. The full list is in Upload pipeline. Only here does the SDK write warning and error itself: it handles the retries internally and does not throw them, so without a log nobody would know.

The messages do not start with S5W — the logger’s channel already says where a line comes from. Exceptions do carry the S5W prefix: an exception may surface far away from the SDK.

Filling in values

The messages are written PSR-3 style, with placeholders: upload {id} started: {name}, …. But PSR-3 only allows a logger to fill values from the context, it does not oblige it to: winter-logger, and Monolog without PsrLogMessageProcessor, print {id} literally. So the SDK fills the values itself, before handing the message to the logger (the internal Support\InterpolatingLogger), and passes the context on unchanged:

text
[DEBUG] (S5W): upload b3b53a23-… started: backup.sql, 100725660 bytes in chunks of 8388608
           {"id":"b3b53a23-…","name":"backup.sql","size":100725660,"chunk":8388608}

The line reads right with any logger, and loggers that index fields (ELK and the like) get them separately. A logger that can fill values itself breaks nothing: the message has no placeholders left.

A full HTTP trace

If you need headers and bodies too, attach winter-cast’s middleware to your own client:

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

The SDK’s own lines stay. Mind that a full trace may record what the SDK deliberately does not.

The key

The key is a secret, and the SDK makes sure it does not leak:

  • it does not reach logs — headers are written neither in request lines nor in events;
  • it does not reach debug output whole — var_dump($config) and print_r($config) show s5w_…aaaa (__debugInfo());
  • it is not in exceptions — the message holds only the path and the server’s answer.

S5wConfig itself keeps the key in the public apiKey property — it is needed to send requests. Do not serialise the configuration or hand it out.