diff --git a/.talismanrc b/.talismanrc index b1596ce..6cfd212 100644 --- a/.talismanrc +++ b/.talismanrc @@ -24,3 +24,5 @@ fileignoreconfig: checksum: b63897181a8cb5993d1305248cfc3e711c4039b5677b6c1e4e2a639e4ecb391b - filename: Contentstack.Core.Tests/RegionHandlerTest.cs checksum: 69899138754908e156aa477d775d12fd6b3fefc1a6c2afec22cb409bd6e6446c +- filename: CHANGELOG.md + checksum: bc17fd4cf564e524c686a8271033f8e6e7f5f69de8137007d1c72d5f563fe92a \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index db238bf..776e063 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,25 @@ +### Version: 2.25.2 +#### Date: Nov-13-2025 + +##### Fix: +- Error Handling + - Fixed error message extraction from Contentstack API responses across all model classes + - HTTP request errors now properly extract and display actual API error messages instead of generic exception messages + - Improved error handling in Query, Entry, Asset, GlobalField, ContentType, AssetLibrary, GlobalFieldQuery, and Taxonomy classes + - Users will now see meaningful error messages (e.g., "Invalid API key", "Entry not found") instead of generic "Exception of type 'ContentstackException' was thrown" messages + - ErrorCode, StatusCode, and Errors dictionary are now properly populated from API responses + ### Version: 2.25.1 #### Date: Nov-10-2025 ##### Enh: - Improved Error messages +##### Fix: +- Taxonomy + - Fixed NullReferenceExceptions + - Fixed InvalidCastException in `GetContentstackError` when exception is not a WebException + - Fixed JsonReaderException in `GetContentstackError` when response is not valid JSON + - All exceptions now properly throw TaxonomyException (extends ContentstackException) with descriptive error messages ### Version: 2.25.0 #### Date: Jan-07-2025 diff --git a/Contentstack.Core.Unit.Tests/AssetLibraryUnitTests.cs b/Contentstack.Core.Unit.Tests/AssetLibraryUnitTests.cs index a85f467..1797d2b 100644 --- a/Contentstack.Core.Unit.Tests/AssetLibraryUnitTests.cs +++ b/Contentstack.Core.Unit.Tests/AssetLibraryUnitTests.cs @@ -549,6 +549,40 @@ public void GetContentstackError_WithGenericException_ReturnsContentstackExcepti Assert.Equal("Test error", result.Message); } + [Fact] + public void GetContentstackError_WithWebException_HandlesExceptionCorrectly() + { + // Arrange + var method = typeof(AssetLibrary).GetMethod("GetContentstackError", + BindingFlags.NonPublic | BindingFlags.Static); + var webEx = new System.Net.WebException("Test error"); + + // Act + var result = method?.Invoke(null, new object[] { webEx }) as ContentstackException; + + // Assert + Assert.NotNull(result); + // When WebException has no response, it should fall back to ex.Message + Assert.NotNull(result.Message); + } + + [Fact] + public void ErrorHandling_WithWebException_ExtractsErrorMessage() + { + // Arrange + var method = typeof(AssetLibrary).GetMethod("GetContentstackError", + BindingFlags.NonPublic | BindingFlags.Static); + var errorMessage = "Asset library error"; + var ex = new Exception(errorMessage); + + // Act + var result = method?.Invoke(null, new object[] { ex }) as ContentstackException; + + // Assert + Assert.NotNull(result); + Assert.Equal(errorMessage, result.Message); + } + [Fact] public void GetHeader_WithNullLocalHeader_ReturnsStackHeaders() { diff --git a/Contentstack.Core.Unit.Tests/AssetUnitTests.cs b/Contentstack.Core.Unit.Tests/AssetUnitTests.cs index 107cb10..f560c83 100644 --- a/Contentstack.Core.Unit.Tests/AssetUnitTests.cs +++ b/Contentstack.Core.Unit.Tests/AssetUnitTests.cs @@ -536,6 +536,40 @@ public void GetContentstackError_WithGenericException_ReturnsContentstackExcepti Assert.Equal("Test error", result.Message); } + [Fact] + public void GetContentstackError_WithWebException_HandlesExceptionCorrectly() + { + // Arrange + var method = typeof(Asset).GetMethod("GetContentstackError", + BindingFlags.NonPublic | BindingFlags.Static); + var webEx = new System.Net.WebException("Test error"); + + // Act + var result = method?.Invoke(null, new object[] { webEx }) as ContentstackException; + + // Assert + Assert.NotNull(result); + // When WebException has no response, it should fall back to ex.Message + Assert.NotNull(result.Message); + } + + [Fact] + public void ErrorHandling_WithWebException_ExtractsErrorMessage() + { + // Arrange + var method = typeof(Asset).GetMethod("GetContentstackError", + BindingFlags.NonPublic | BindingFlags.Static); + var errorMessage = "Asset processing failed"; + var ex = new Exception(errorMessage); + + // Act + var result = method?.Invoke(null, new object[] { ex }) as ContentstackException; + + // Assert + Assert.NotNull(result); + Assert.Equal(errorMessage, result.Message); + } + [Fact] public void GetHeader_WithNullLocalHeader_ReturnsStackHeaders() { diff --git a/Contentstack.Core.Unit.Tests/ContentTypeUnitTests.cs b/Contentstack.Core.Unit.Tests/ContentTypeUnitTests.cs index 8d24b77..0f83efb 100644 --- a/Contentstack.Core.Unit.Tests/ContentTypeUnitTests.cs +++ b/Contentstack.Core.Unit.Tests/ContentTypeUnitTests.cs @@ -311,6 +311,36 @@ public void GetContentstackError_WithGenericException_ReturnsContentstackExcepti Assert.IsType(result); } + [Fact] + public void GetContentstackError_WithGenericException_ReturnsExceptionWithCorrectMessage() + { + // Arrange + var errorMessage = "Content type error"; + var exception = new Exception(errorMessage); + + // Act + var result = ContentType.GetContentstackError(exception); + + // Assert + Assert.NotNull(result); + Assert.Equal(errorMessage, result.Message); + } + + [Fact] + public void GetContentstackError_WithWebException_HandlesExceptionCorrectly() + { + // Arrange + var webEx = new System.Net.WebException("Test error"); + + // Act + var result = ContentType.GetContentstackError(webEx); + + // Assert + Assert.NotNull(result); + // When WebException has no response, it should fall back to ex.Message + Assert.NotNull(result.Message); + } + [Fact] public void Fetch_WithIncludeBranch_VerifiesQueryParameters() { diff --git a/Contentstack.Core.Unit.Tests/EntryUnitTests.cs b/Contentstack.Core.Unit.Tests/EntryUnitTests.cs index d7d22eb..15813c2 100644 --- a/Contentstack.Core.Unit.Tests/EntryUnitTests.cs +++ b/Contentstack.Core.Unit.Tests/EntryUnitTests.cs @@ -1704,6 +1704,42 @@ public void GetContentstackError_WithGenericException_ReturnsContentstackExcepti Assert.Equal("Test error", result.Message); } + [Fact] + public void GetContentstackError_WithWebException_HandlesExceptionCorrectly() + { + // Arrange + var method = typeof(Entry).GetMethod("GetContentstackError", + BindingFlags.NonPublic | BindingFlags.Static); + var webEx = new System.Net.WebException("Test error"); + + // Act + var result = method?.Invoke(null, new object[] { webEx }) as ContentstackException; + + // Assert + Assert.NotNull(result); + // When WebException has no response, it should fall back to ex.Message + Assert.NotNull(result.Message); + } + + [Fact] + public void ErrorHandling_WithWebException_ExtractsErrorMessage() + { + // Arrange + // This test verifies that the error handling logic properly checks for WebException + // and calls GetContentstackError to extract error messages + var method = typeof(Entry).GetMethod("GetContentstackError", + BindingFlags.NonPublic | BindingFlags.Static); + var errorMessage = "Custom error message"; + var ex = new Exception(errorMessage); + + // Act + var result = method?.Invoke(null, new object[] { ex }) as ContentstackException; + + // Assert + Assert.NotNull(result); + Assert.Equal(errorMessage, result.Message); + } + [Fact] public void GetHeader_WithNullLocalHeader_ReturnsFormHeaders() { diff --git a/Contentstack.Core.Unit.Tests/GlobalFieldQueryUnitTests.cs b/Contentstack.Core.Unit.Tests/GlobalFieldQueryUnitTests.cs index 604c3df..26a9670 100644 --- a/Contentstack.Core.Unit.Tests/GlobalFieldQueryUnitTests.cs +++ b/Contentstack.Core.Unit.Tests/GlobalFieldQueryUnitTests.cs @@ -142,6 +142,36 @@ public void GetContentstackError_WithGenericException_ReturnsContentstackExcepti Assert.IsType(result); } + [Fact] + public void GetContentstackError_WithGenericException_ReturnsExceptionWithCorrectMessage() + { + // Arrange + var errorMessage = "Global field query error"; + var exception = new Exception(errorMessage); + + // Act + var result = GlobalFieldQuery.GetContentstackError(exception); + + // Assert + Assert.NotNull(result); + Assert.Equal(errorMessage, result.Message); + } + + [Fact] + public void GetContentstackError_WithWebException_HandlesExceptionCorrectly() + { + // Arrange + var webEx = new System.Net.WebException("Test error"); + + // Act + var result = GlobalFieldQuery.GetContentstackError(webEx); + + // Assert + Assert.NotNull(result); + // When WebException has no response, it should fall back to ex.Message + Assert.NotNull(result.Message); + } + [Fact] public void Find_WithMultipleParameters_VerifiesAllQueryParameters() { diff --git a/Contentstack.Core.Unit.Tests/GlobalFieldUnitTests.cs b/Contentstack.Core.Unit.Tests/GlobalFieldUnitTests.cs index 0d9396b..03d5800 100644 --- a/Contentstack.Core.Unit.Tests/GlobalFieldUnitTests.cs +++ b/Contentstack.Core.Unit.Tests/GlobalFieldUnitTests.cs @@ -138,6 +138,75 @@ public void RemoveHeader_RemovesHeader() } #endregion + + #region GetContentstackError Tests + + [Fact] + public void GetContentstackError_WithWebException_ReturnsContentstackException() + { + // Arrange + var method = typeof(GlobalField).GetMethod("GetContentstackError", + BindingFlags.NonPublic | BindingFlags.Static); + var webEx = new System.Net.WebException("Test error"); + + // Act + var result = method?.Invoke(null, new object[] { webEx }) as Contentstack.Core.Internals.ContentstackException; + + // Assert + Assert.NotNull(result); + } + + [Fact] + public void GetContentstackError_WithGenericException_ReturnsContentstackException() + { + // Arrange + var method = typeof(GlobalField).GetMethod("GetContentstackError", + BindingFlags.NonPublic | BindingFlags.Static); + var ex = new Exception("Test error"); + + // Act + var result = method?.Invoke(null, new object[] { ex }) as Contentstack.Core.Internals.ContentstackException; + + // Assert + Assert.NotNull(result); + Assert.Equal("Test error", result.Message); + } + + [Fact] + public void GetContentstackError_WithWebException_HandlesExceptionCorrectly() + { + // Arrange + var method = typeof(GlobalField).GetMethod("GetContentstackError", + BindingFlags.NonPublic | BindingFlags.Static); + var webEx = new System.Net.WebException("Test error"); + + // Act + var result = method?.Invoke(null, new object[] { webEx }) as Contentstack.Core.Internals.ContentstackException; + + // Assert + Assert.NotNull(result); + // When WebException has no response, it should fall back to ex.Message + Assert.NotNull(result.Message); + } + + [Fact] + public void ErrorHandling_WithWebException_ExtractsErrorMessage() + { + // Arrange + var method = typeof(GlobalField).GetMethod("GetContentstackError", + BindingFlags.NonPublic | BindingFlags.Static); + var errorMessage = "Global field error"; + var ex = new Exception(errorMessage); + + // Act + var result = method?.Invoke(null, new object[] { ex }) as Contentstack.Core.Internals.ContentstackException; + + // Assert + Assert.NotNull(result); + Assert.Equal(errorMessage, result.Message); + } + + #endregion } } diff --git a/Contentstack.Core.Unit.Tests/QueryUnitTests.cs b/Contentstack.Core.Unit.Tests/QueryUnitTests.cs index f89cd7d..316f19f 100644 --- a/Contentstack.Core.Unit.Tests/QueryUnitTests.cs +++ b/Contentstack.Core.Unit.Tests/QueryUnitTests.cs @@ -1447,6 +1447,42 @@ public void GetContentstackError_WithGenericException_ReturnsContentstackExcepti Assert.IsType(result); } + [Fact] + public void GetContentstackError_WithGenericException_ReturnsExceptionWithCorrectMessage() + { + // Arrange + var method = typeof(Query).GetMethod("GetContentstackError", + BindingFlags.NonPublic | BindingFlags.Static); + var errorMessage = "Test error message"; + var ex = new Exception(errorMessage); + + // Act + var result = method?.Invoke(null, new object[] { ex }) as ContentstackException; + + // Assert + Assert.NotNull(result); + Assert.Equal(errorMessage, result.Message); + } + + [Fact] + public void ErrorHandling_WithWebException_CallsGetContentstackError() + { + // Arrange + // This test verifies that when a WebException is caught, GetContentstackError is called + // We can't easily mock a WebException with a response, but we can verify the logic path + var method = typeof(Query).GetMethod("GetContentstackError", + BindingFlags.NonPublic | BindingFlags.Static); + var webEx = new System.Net.WebException("Test error"); + + // Act + var result = method?.Invoke(null, new object[] { webEx }) as ContentstackException; + + // Assert + Assert.NotNull(result); + // When WebException has no response, it should fall back to ex.Message + Assert.NotNull(result.Message); + } + [Fact] public void And_WithExistingAndKey_ReplacesAndValue() { diff --git a/Contentstack.Core/Models/Asset.cs b/Contentstack.Core/Models/Asset.cs index 79dbd3b..26a674b 100644 --- a/Contentstack.Core/Models/Asset.cs +++ b/Contentstack.Core/Models/Asset.cs @@ -424,6 +424,16 @@ public async Task Fetch() } catch (Exception ex) { + if (ex is System.Net.WebException) + { + var contentstackError = GetContentstackError(ex); + throw new AssetException(contentstackError.Message, ex) + { + ErrorCode = contentstackError.ErrorCode, + StatusCode = contentstackError.StatusCode, + Errors = contentstackError.Errors + }; + } throw AssetException.CreateForProcessingError(ex); } } diff --git a/Contentstack.Core/Models/AssetLibrary.cs b/Contentstack.Core/Models/AssetLibrary.cs index 8c189a8..24c33f2 100644 --- a/Contentstack.Core/Models/AssetLibrary.cs +++ b/Contentstack.Core/Models/AssetLibrary.cs @@ -580,6 +580,16 @@ private async Task Exec() } catch (Exception ex) { + if (ex is System.Net.WebException) + { + var contentstackError = GetContentstackError(ex); + throw new AssetException(contentstackError.Message, ex) + { + ErrorCode = contentstackError.ErrorCode, + StatusCode = contentstackError.StatusCode, + Errors = contentstackError.Errors + }; + } throw AssetException.CreateForProcessingError(ex); } } diff --git a/Contentstack.Core/Models/ContentType.cs b/Contentstack.Core/Models/ContentType.cs index ca9a9a3..a87f7c9 100644 --- a/Contentstack.Core/Models/ContentType.cs +++ b/Contentstack.Core/Models/ContentType.cs @@ -175,6 +175,16 @@ public async Task Fetch(Dictionary param = null) } catch (Exception ex) { + if (ex is System.Net.WebException) + { + var contentstackError = GetContentstackError(ex); + throw new ContentTypeException(contentstackError.Message, ex) + { + ErrorCode = contentstackError.ErrorCode, + StatusCode = contentstackError.StatusCode, + Errors = contentstackError.Errors + }; + } throw ContentTypeException.CreateForProcessingError(ex); } } diff --git a/Contentstack.Core/Models/Entry.cs b/Contentstack.Core/Models/Entry.cs index 527b919..882a54c 100644 --- a/Contentstack.Core/Models/Entry.cs +++ b/Contentstack.Core/Models/Entry.cs @@ -1445,6 +1445,16 @@ public async Task Fetch() } catch (Exception ex) { + if (ex is System.Net.WebException) + { + var contentstackError = GetContentstackError(ex); + throw new EntryException(contentstackError.Message, ex) + { + ErrorCode = contentstackError.ErrorCode, + StatusCode = contentstackError.StatusCode, + Errors = contentstackError.Errors + }; + } throw EntryException.CreateForProcessingError(ex); } } diff --git a/Contentstack.Core/Models/GlobalField.cs b/Contentstack.Core/Models/GlobalField.cs index 6280770..46c3943 100644 --- a/Contentstack.Core/Models/GlobalField.cs +++ b/Contentstack.Core/Models/GlobalField.cs @@ -188,6 +188,16 @@ public async Task Fetch(Dictionary param = null) } catch (Exception ex) { + if (ex is System.Net.WebException) + { + var contentstackError = GetContentstackError(ex); + throw new GlobalFieldException(contentstackError.Message, ex) + { + ErrorCode = contentstackError.ErrorCode, + StatusCode = contentstackError.StatusCode, + Errors = contentstackError.Errors + }; + } throw GlobalFieldException.CreateForProcessingError(ex); } } diff --git a/Contentstack.Core/Models/GlobalFieldQuery.cs b/Contentstack.Core/Models/GlobalFieldQuery.cs index b3b5b4b..d804c1a 100644 --- a/Contentstack.Core/Models/GlobalFieldQuery.cs +++ b/Contentstack.Core/Models/GlobalFieldQuery.cs @@ -156,6 +156,16 @@ public async Task Find(Dictionary param = null) } catch (Exception ex) { + if (ex is System.Net.WebException) + { + var contentstackError = GetContentstackError(ex); + throw new ContentstackException(contentstackError.Message, ex) + { + ErrorCode = contentstackError.ErrorCode, + StatusCode = contentstackError.StatusCode, + Errors = contentstackError.Errors + }; + } throw new ContentstackException(ErrorMessages.GlobalFieldQueryError, ex); } } diff --git a/Contentstack.Core/Models/Query.cs b/Contentstack.Core/Models/Query.cs index 66c006f..ea92342 100644 --- a/Contentstack.Core/Models/Query.cs +++ b/Contentstack.Core/Models/Query.cs @@ -1947,6 +1947,16 @@ private async Task Exec() } catch (Exception ex) { + if (ex is System.Net.WebException) + { + var contentstackError = GetContentstackError(ex); + throw new QueryFilterException(contentstackError.Message, ex) + { + ErrorCode = contentstackError.ErrorCode, + StatusCode = contentstackError.StatusCode, + Errors = contentstackError.Errors + }; + } throw QueryFilterException.Create(ex); } } diff --git a/Contentstack.Core/Models/Taxonomy.cs b/Contentstack.Core/Models/Taxonomy.cs index a125833..9d7322f 100644 --- a/Contentstack.Core/Models/Taxonomy.cs +++ b/Contentstack.Core/Models/Taxonomy.cs @@ -21,6 +21,14 @@ protected override string _Url { get { + if (this.Stack == null) + { + throw new TaxonomyException("Taxonomy Stack instance is null. Please ensure the Taxonomy is properly initialized with a ContentstackClient instance."); + } + if (this.Stack.Config == null) + { + throw new TaxonomyException("Taxonomy Stack Config is null. Please ensure the ContentstackClient is properly configured."); + } Config config = this.Stack.Config; return String.Format("{0}/taxonomies/entries", config.BaseUrl); } @@ -40,6 +48,10 @@ internal Taxonomy() } internal Taxonomy(ContentstackClient stack): base(stack) { + if (stack == null) + { + throw new TaxonomyException("ContentstackClient instance cannot be null when creating a Taxonomy instance."); + } this.Stack = stack; this._StackHeaders = stack._LocalHeaders; } @@ -252,30 +264,59 @@ internal static ContentstackException GetContentstackError(Exception ex) try { - System.Net.WebException webEx = (System.Net.WebException)ex; - - using (var exResp = webEx.Response) - using (var stream = exResp.GetResponseStream()) - using (var reader = new StreamReader(stream)) + System.Net.WebException webEx = ex as System.Net.WebException; + + if (webEx != null && webEx.Response != null) { - errorMessage = reader.ReadToEnd(); - JObject data = JObject.Parse(errorMessage.Replace("\r\n", "")); + using (var exResp = webEx.Response) + { + var stream = exResp.GetResponseStream(); + if (stream != null) + { + using (stream) + using (var reader = new StreamReader(stream)) + { + errorMessage = reader.ReadToEnd(); + + if (!string.IsNullOrWhiteSpace(errorMessage)) + { + try + { + JObject data = JObject.Parse(errorMessage.Replace("\r\n", "")); - JToken token = data["error_code"]; - if (token != null) - errorCode = token.Value(); + JToken token = data["error_code"]; + if (token != null) + errorCode = token.Value(); - token = data["error_message"]; - if (token != null) - errorMessage = token.Value(); + token = data["error_message"]; + if (token != null) + errorMessage = token.Value(); - token = data["errors"]; - if (token != null) - errors = token.ToObject>(); + token = data["errors"]; + if (token != null) + errors = token.ToObject>(); + } + catch (Newtonsoft.Json.JsonException) + { + // If JSON parsing fails, use the raw error message + // errorMessage is already set from ReadToEnd() + } + } - var response = exResp as HttpWebResponse; - if (response != null) - statusCode = response.StatusCode; + var response = exResp as HttpWebResponse; + if (response != null) + statusCode = response.StatusCode; + } + } + else + { + errorMessage = webEx.Message; + } + } + } + else + { + errorMessage = ex.Message; } } catch diff --git a/Directory.Build.props b/Directory.Build.props index e15a9a2..0e81c8f 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,5 +1,5 @@ - 2.25.1 + 2.25.2