From 028af5de454511707a3acd269e99497cbf547cc1 Mon Sep 17 00:00:00 2001 From: Harlan Crystal Date: Mon, 29 Jun 2026 13:17:36 -0700 Subject: [PATCH] Add a public StreamApiException constructor for integrator unit tests StreamApiException is a public, catch-and-branch type (consumers inspect StatusCode/Code and branch via the StreamApiExceptionExtensions.Is* helpers), but its only constructor was internal and took the internal APIErrorInternalDTO, so integrators could not construct one to unit-test their own error handling (e.g. simulating a 403 / code 70 'no access to channels' response). Add a public constructor over the type's already-public fields. Both it and the existing DTO constructor funnel through one private constructor and a shared BuildMessage helper, so the Exception.Message format is identical however the exception is built. APIErrorInternalDTO stays internal. --- Assets/Plugins/StreamChat/Changelog.txt | 1 + .../Core/Exceptions/StreamApiException.cs | 59 +++++++++++++++---- 2 files changed, 47 insertions(+), 13 deletions(-) diff --git a/Assets/Plugins/StreamChat/Changelog.txt b/Assets/Plugins/StreamChat/Changelog.txt index 3a66b3e1..48df5147 100644 --- a/Assets/Plugins/StreamChat/Changelog.txt +++ b/Assets/Plugins/StreamChat/Changelog.txt @@ -1,6 +1,7 @@ Unreleased: Features: +* Add a public StreamApiException constructor (statusCode, code, errorMessage, moreInfo, duration, exceptionFields). StreamApiException is a public, catch-and-branch type (via the StreamApiExceptionExtensions.Is* helpers), but until now it could only be constructed inside the SDK from the internal APIErrorInternalDTO, so integrators could not build one to unit-test their own error handling (e.g. simulating a 403 / code 70 "no access to channels" response). The new constructor maps directly to the type's public properties and keeps APIErrorInternalDTO internal. * Add IStreamClientConfig.OptimisticMessageInsert (default true). When true (the existing behavior), a message you send is inserted into the local channel state and raised via IStreamChannel.MessageReceived immediately, before the server's message.new echo arrives. Set it to false to skip the optimistic local insert and wait for the server echo instead, so every participant - including the sender - observes messages in the same server-defined order. Useful when consistent cross-client ordering matters more than instant local feedback (e.g. a shared, broadcast-ordered feed). v5.5.0: diff --git a/Assets/Plugins/StreamChat/Core/Exceptions/StreamApiException.cs b/Assets/Plugins/StreamChat/Core/Exceptions/StreamApiException.cs index 75f42418..f021996f 100644 --- a/Assets/Plugins/StreamChat/Core/Exceptions/StreamApiException.cs +++ b/Assets/Plugins/StreamChat/Core/Exceptions/StreamApiException.cs @@ -79,18 +79,46 @@ public class StreamApiException : Exception public IReadOnlyDictionary ExceptionFields => _exceptionFields; internal StreamApiException(APIErrorInternalDTO apiError) - : base( - $"{apiError.Message}, Error Code: {apiError.Code}, Http Status Code: {apiError.StatusCode}, More info: {apiError.MoreInfo}, Exception fields: {PrintExceptionFields(apiError)}") + : this(apiError.StatusCode, apiError.Code, apiError.Message, apiError.MoreInfo, apiError.Duration, + apiError.ExceptionFields) { - StatusCode = apiError.StatusCode; - Code = apiError.Code; - Duration = apiError.Duration; - ErrorMessage = apiError.Message; - MoreInfo = apiError.MoreInfo; + } - if (apiError.ExceptionFields != null && apiError.ExceptionFields.Count > 0) + /// + /// Construct an exception representing a specific API error. Intended for integrators who need to + /// exercise their own handling in unit tests (e.g. simulating a + /// 403 / code 70 "no access to channels" response) without depending on the internal + /// type. Every argument maps directly to a public property of this type. + /// + public StreamApiException( + int statusCode, + int code, + string errorMessage = null, + string moreInfo = null, + string duration = null, + IReadOnlyDictionary exceptionFields = null) + : this((int?)statusCode, code, errorMessage, moreInfo, duration, exceptionFields) + { + } + + private StreamApiException( + int? statusCode, + int? code, + string errorMessage, + string moreInfo, + string duration, + IReadOnlyDictionary exceptionFields) + : base(BuildMessage(errorMessage, code, statusCode, moreInfo, exceptionFields)) + { + StatusCode = statusCode; + Code = code; + Duration = duration; + ErrorMessage = errorMessage; + MoreInfo = moreInfo; + + if (exceptionFields != null && exceptionFields.Count > 0) { - _exceptionFields = new Dictionary(apiError.ExceptionFields); + _exceptionFields = new Dictionary(exceptionFields); } } @@ -98,18 +126,23 @@ internal StreamApiException(APIErrorInternalDTO apiError) private readonly Dictionary _exceptionFields; - private static string PrintExceptionFields(APIErrorInternalDTO apiError) + // Shared by both constructors so the message format stays identical however the exception is built. + private static string BuildMessage(string message, int? code, int? statusCode, string moreInfo, + IReadOnlyDictionary exceptionFields) + => $"{message}, Error Code: {code}, Http Status Code: {statusCode}, More info: {moreInfo}, Exception fields: {PrintExceptionFields(exceptionFields)}"; + + private static string PrintExceptionFields(IReadOnlyDictionary exceptionFields) { - if (apiError.ExceptionFields == null) + if (exceptionFields == null) { return "None"; } _sb.Length = 0; - var count = apiError.ExceptionFields.Count; + var count = exceptionFields.Count; var index = 0; - foreach (var keyValuePair in apiError.ExceptionFields) + foreach (var keyValuePair in exceptionFields) { _sb.Append(keyValuePair.Key); _sb.Append(": ");