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 db9a175..776e063 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,13 @@ ### 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 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/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