All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
- Connection pool configuration on
ClientBuilder(CHA-2956). Four new chained methods, allintseconds:maxConnsPerHost(int): default5(per-host concurrency cap via curlCURLMOPT_MAX_HOST_CONNECTIONSon a persistent multi handle; effective in long-running PHP runtimes)idleTimeout(int): default55(per-connection lifetime cap viaCURLOPT_MAXLIFETIME_CONN; effective in long-running PHP runtimes; requires libcurl 7.80.0 or later)connectTimeout(int): default10(Guzzleconnect_timeout)requestTimeout(int): default30(Guzzletimeout)
GetStream\Http\PoolConfigimmutable value object holding the 5 canonical knobs.HttpClientInterface::request()gains an optional 5tharray $options = []parameter for per-call overrides (e.g.,['timeout' => 2]). Backward-compatible.- INFO log on
ClientBuilder::build()listing the effective pool config. Emitted viaerror_log(); suppressed in PHPUnit runs. GuzzleHttpClient::getPoolConfig()accessor for diagnostics.
GuzzleHttpClient::__construct()gains an optional 3rd?PoolConfig $poolparameter. Existing callsites continue to work unchanged.
maxConnsPerHost and idleTimeout are enforced via libcurl's multi-handle pool (CURLMOPT_MAX_HOST_CONNECTIONS) and connection lifetime cap (CURLOPT_MAXLIFETIME_CONN). They take effect only when the SDK client is reused across requests within a single PHP process: long-running runtimes such as Swoole, RoadRunner, ReactPHP, and CLI daemons. Instantiate the SDK client once and reuse it.
Under PHP-FPM (and one-shot CLI scripts) the PHP process exits at the end of each request, taking the multi handle and any pooled connections with it. The per-call request and connect timeouts still apply; pool sizing and idle cycling have no cross-request effect because there is nothing to keep alive between requests.
CURLOPT_MAXLIFETIME_CONN requires libcurl 7.80.0 (Nov 2021) or later. If your PHP build links against an older libcurl, pooling still works without active lifetime cycling.
- No env-var overrides.
- No PSR-3 logger injection; INFO log goes through
error_log().
StreamApiExceptionstructured fields are reshaped to match the canonicalAPIErrorenvelope. The constructor signature changes:(string $message, int $statusCode, int $code, array $exceptionFields, bool $unrecoverable, string $rawResponseBody, ?string $moreInfo, mixed $details, ?\Throwable $previous). Replaced accessors:getResponseBody(): ?string→getRawResponseBody(): stringgetErrorDetails(): array(non-canonical bag) →getExceptionFields(): array<string,string>(only the validation map fromexception_fields)- New:
isUnrecoverable(): bool,getMoreInfo(): ?string,getDetails(): mixed. getStatusCode()andgetCode()keep their existing semantics — both return the HTTP status (back-compat with pre-CHA-2958 callers that branched on$e->getCode() === 429). The canonicalAPIError.codeis exposed via the newgetApiErrorCode(): int.
-
Error-handling spec rollout (CHA-2958, spec):
- New
StreamRateLimitException extends StreamApiExceptionfor HTTP 429 responses. ExposesgetRetryAfter(): ?int(seconds;nullwhen the header is absent or unparseable). Both integer seconds (Retry-After: 30) and HTTP-date forms (Retry-After: Fri, 31 Dec 2026 23:59:59 GMT) are accepted per RFC 7231 §7.1.3; HTTP-date deltas are clamped to ≥ 0. - New
StreamTransportException extends StreamExceptionfor network-layer failures with no HTTP response (connection reset, timeout, TLS handshake failure, DNS failure). ExposesgetErrorType(): stringreturning one ofconnection_reset·timeout·dns_failure·tls_handshake_failed·unknown(matches the logging spec'serror.typeenum). The original Guzzle exception is preserved viagetPrevious(). - New
StreamTaskException extends StreamExceptionthrown byClient::waitForTask()when a polled task settles intostatus: "failed". CarriesgetTaskId(),getErrorType(),getDescription(),getStackTrace(),getVersion()from the task'sErrorResultpayload. - New
Client::waitForTask(string $taskId, int $pollIntervalSeconds = 1, int $timeoutSeconds = 60). Polls/api/v2/tasks/{id}until the task settles intocompleted(returnsGetTaskResponse) orfailed(throwsStreamTaskException). On timeout it raisesStreamTransportExceptionwitherrorType = "timeout".
- New
-
Cause-chain preservation: every wrap point in
GuzzleHttpClientnow passes the caughtGuzzleExceptionas the$previousargument to the SDK exception, fixing the broken chain in the priorGuzzleHttpClient::request()catch block. Unparseable error responses (HTTP layer succeeded, body is not a validAPIError) wrap a\JsonExceptioncause and surface as a baseStreamApiExceptionwithcode = 0andmessage = "failed to parse error response". -
Webhook handling spec helpers (CHA-2961):
UnknownEventclass for forward-compat;gunzipPayload,decodeSqsPayload,decodeSnsPayloadprimitives;verifyAndParseWebhookHTTP composite;parseSqs/parseSnsqueue composites (no signature: backend emits no HMAC for queue messages today; trust is established via AWS IAM controls on the SQS queue / SNS topic). Transparent gzip via magic-byte detection. -
New
GetStream\Webhooknamespace alias (preferred);GetStream\Generated\Webhookretained as backward-compat alias. PSR-4 shim (src/Webhook.php) ensures the canonical name resolves on first touch. -
New exception class:
GetStream\Exceptions\InvalidWebhookException(unified, covering signature mismatches, parse failures, decompression errors, etc.). -
New
GetStream\Models\UnknownEventclass. -
New instance methods on
GetStream\Client:verifySignature($body, $signature)andverifyAndParseWebhook($body, $signature)that drop the api_secret parameter in favor of the client's stored secret. Dual API: static methods remain available. -
New instance methods on
GetStream\Client:parseSqs(string $messageBody),parseSns(string $notificationBody)(no signature; AWS IAM). -
Conformance fixture suite under
tests/fixtures/webhooks/.
- No breaking changes.
- Type names across all products now follow the OpenAPI spec naming convention: response types are suffixed with
Response, input types withRequest. See MIGRATION_v3_to_v4.md for the complete rename mapping. Event(WebSocket envelope type) renamed toWSEvent. Base event type renamed fromBaseEventtoEvent(with fieldtypeinstead ofT).- Event composition changed from monolithic
*Presetembeds to modularHas*types. Pagerrenamed toPagerResponseand migrated from offset-based to cursor-based pagination (next/prevtokens).
- Full product coverage: Chat, Video, Moderation, and Feeds APIs are all supported in a single SDK.
- Feeds: activities, feeds, feed groups, follows, comments, reactions, collections, bookmarks, membership levels, feed views and more.
- Video: calls, recordings, transcription, closed captions, SFU, call statistics, user feedback analytics, and more.
- Moderation: flags, review queue, moderation rules, config, appeals, moderation logs, and more.
- Push notification types, preferences, and templates.
- Webhook support:
WHEventenvelope class for receiving webhook payloads, utility functions for decoding and verifying webhook signatures, and a full set of individual typed event classes for every event across all products (Chat, Video, Moderation, Feeds) usable as discriminated event types. - Cursor-based pagination across all list endpoints.