diff --git a/README.md b/README.md index cfa68ecff..fd868ab4e 100644 --- a/README.md +++ b/README.md @@ -1099,8 +1099,9 @@ var response = await gitHubApi.GetUser("octocat"); //Getting the status code (returns a value from the System.Net.HttpStatusCode enumeration) var httpStatus = response.StatusCode; -//Determining if a success status code was received -if(response.IsSuccessStatusCode) +//Determining if a success status code was received and there wasn't any other error +//(for example, during content deserialization) +if(response.IsSuccessful) { //YAY! Do the thing... } @@ -1362,7 +1363,7 @@ You can then decide what to do like so: ```csharp var response = await _myRefitClient.GetSomeStuff(); -if(response.IsSuccessStatusCode) +if(response.IsSuccessful) { //do your thing } @@ -1372,6 +1373,9 @@ else } ``` +> [!NOTE] +> The `IsSuccessful` property checks whether the response status code is in the range 200-299 and there wasn't any other error (for example, during content deserialization). If you just want to check the HTTP response status code, you can use the `IsSuccessStatusCode` property. + #### When returning `Task` Refit throws any `ApiException` raised by the `ExceptionFactory` when processing the response and any errors that occur when attempting to deserialize the response to `Task`. diff --git a/Refit.Tests/API/ApiApprovalTests.Refit.DotNet6_0.verified.txt b/Refit.Tests/API/ApiApprovalTests.Refit.DotNet6_0.verified.txt index 34fd210ce..aca0a3307 100644 --- a/Refit.Tests/API/ApiApprovalTests.Refit.DotNet6_0.verified.txt +++ b/Refit.Tests/API/ApiApprovalTests.Refit.DotNet6_0.verified.txt @@ -40,12 +40,17 @@ namespace Refit public Refit.ApiException? Error { get; } public System.Net.Http.Headers.HttpResponseHeaders Headers { get; } [System.Diagnostics.CodeAnalysis.MemberNotNullWhen(false, "Error")] + [System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "ContentHeaders")] + [get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(false, "Error")] + [get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "ContentHeaders")] + public bool IsSuccessStatusCode { get; } + [System.Diagnostics.CodeAnalysis.MemberNotNullWhen(false, "Error")] [System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "Content")] [System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "ContentHeaders")] [get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(false, "Error")] [get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "Content")] [get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "ContentHeaders")] - public bool IsSuccessStatusCode { get; } + public bool IsSuccessful { get; } public string? ReasonPhrase { get; } public System.Net.Http.HttpRequestMessage? RequestMessage { get; } public Refit.RefitSettings Settings { get; } @@ -53,6 +58,7 @@ namespace Refit public System.Version Version { get; } public void Dispose() { } public System.Threading.Tasks.Task> EnsureSuccessStatusCodeAsync() { } + public System.Threading.Tasks.Task> EnsureSuccessfulAsync() { } } [System.AttributeUsage(System.AttributeTargets.Property | System.AttributeTargets.Parameter)] [System.Obsolete("Use Refit.StreamPart, Refit.ByteArrayPart, Refit.FileInfoPart or if necessary, in" + @@ -190,6 +196,11 @@ namespace Refit [get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(false, "Error")] [get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "ContentHeaders")] bool IsSuccessStatusCode { get; } + [System.Diagnostics.CodeAnalysis.MemberNotNullWhen(false, "Error")] + [System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "ContentHeaders")] + [get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(false, "Error")] + [get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "ContentHeaders")] + bool IsSuccessful { get; } string? ReasonPhrase { get; } System.Net.Http.HttpRequestMessage? RequestMessage { get; } System.Net.HttpStatusCode StatusCode { get; } @@ -201,12 +212,17 @@ namespace Refit new System.Net.Http.Headers.HttpContentHeaders? ContentHeaders { get; } new Refit.ApiException? Error { get; } [System.Diagnostics.CodeAnalysis.MemberNotNullWhen(false, "Error")] + [System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "ContentHeaders")] + [get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(false, "Error")] + [get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "ContentHeaders")] + new bool IsSuccessStatusCode { get; } + [System.Diagnostics.CodeAnalysis.MemberNotNullWhen(false, "Error")] [System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "Content")] [System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "ContentHeaders")] [get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(false, "Error")] [get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "Content")] [get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "ContentHeaders")] - new bool IsSuccessStatusCode { get; } + new bool IsSuccessful { get; } } public interface IFormUrlEncodedParameterFormatter { diff --git a/Refit.Tests/API/ApiApprovalTests.Refit.DotNet8_0.verified.txt b/Refit.Tests/API/ApiApprovalTests.Refit.DotNet8_0.verified.txt index 182ebd24d..7706d4756 100644 --- a/Refit.Tests/API/ApiApprovalTests.Refit.DotNet8_0.verified.txt +++ b/Refit.Tests/API/ApiApprovalTests.Refit.DotNet8_0.verified.txt @@ -40,12 +40,17 @@ namespace Refit public Refit.ApiException? Error { get; } public System.Net.Http.Headers.HttpResponseHeaders Headers { get; } [System.Diagnostics.CodeAnalysis.MemberNotNullWhen(false, "Error")] + [System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "ContentHeaders")] + [get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(false, "Error")] + [get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "ContentHeaders")] + public bool IsSuccessStatusCode { get; } + [System.Diagnostics.CodeAnalysis.MemberNotNullWhen(false, "Error")] [System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "Content")] [System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "ContentHeaders")] [get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(false, "Error")] [get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "Content")] [get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "ContentHeaders")] - public bool IsSuccessStatusCode { get; } + public bool IsSuccessful { get; } public string? ReasonPhrase { get; } public System.Net.Http.HttpRequestMessage? RequestMessage { get; } public Refit.RefitSettings Settings { get; } @@ -53,6 +58,7 @@ namespace Refit public System.Version Version { get; } public void Dispose() { } public System.Threading.Tasks.Task> EnsureSuccessStatusCodeAsync() { } + public System.Threading.Tasks.Task> EnsureSuccessfulAsync() { } } [System.AttributeUsage(System.AttributeTargets.Property | System.AttributeTargets.Parameter)] [System.Obsolete("Use Refit.StreamPart, Refit.ByteArrayPart, Refit.FileInfoPart or if necessary, in" + @@ -190,6 +196,11 @@ namespace Refit [get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(false, "Error")] [get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "ContentHeaders")] bool IsSuccessStatusCode { get; } + [System.Diagnostics.CodeAnalysis.MemberNotNullWhen(false, "Error")] + [System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "ContentHeaders")] + [get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(false, "Error")] + [get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "ContentHeaders")] + bool IsSuccessful { get; } string? ReasonPhrase { get; } System.Net.Http.HttpRequestMessage? RequestMessage { get; } System.Net.HttpStatusCode StatusCode { get; } @@ -201,12 +212,17 @@ namespace Refit new System.Net.Http.Headers.HttpContentHeaders? ContentHeaders { get; } new Refit.ApiException? Error { get; } [System.Diagnostics.CodeAnalysis.MemberNotNullWhen(false, "Error")] + [System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "ContentHeaders")] + [get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(false, "Error")] + [get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "ContentHeaders")] + new bool IsSuccessStatusCode { get; } + [System.Diagnostics.CodeAnalysis.MemberNotNullWhen(false, "Error")] [System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "Content")] [System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "ContentHeaders")] [get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(false, "Error")] [get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "Content")] [get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "ContentHeaders")] - new bool IsSuccessStatusCode { get; } + new bool IsSuccessful { get; } } public interface IFormUrlEncodedParameterFormatter { diff --git a/Refit.Tests/ResponseTests.cs b/Refit.Tests/ResponseTests.cs index 0b7149f2a..ed6ff63c6 100644 --- a/Refit.Tests/ResponseTests.cs +++ b/Refit.Tests/ResponseTests.cs @@ -174,6 +174,77 @@ public async Task When_BadRequest_EnsureSuccessStatusCodeAsync_ThrowsValidationE Assert.Equal("type", actualException.Content.Type); } + /// + /// Test to verify if IsSuccessful returns false if we have a success status code, but there is a deserialization error + /// + [Fact] + public async Task When_SerializationErrorOnSuccessStatusCode_IsSuccessful_ShouldReturnFalse() + { + var expectedResponse = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent("Invalid JSON") + }; + + mockHandler + .Expect(HttpMethod.Get, "http://api/GetApiResponseTestObject") + .Respond(req => expectedResponse); + + using var response = await fixture.GetApiResponseTestObject(); + + Assert.True(response.IsSuccessStatusCode); + Assert.False(response.IsSuccessful); + Assert.NotNull(response.Error); + } + + /// + /// Test to verify if EnsureSuccessStatusCodeAsync do not throw an ApiException if we have a success status code, but there is a deserialization error + /// + [Fact] + public async Task When_SerializationErrorOnSuccessStatusCode_EnsureSuccesStatusCodeAsync_DoNotThrowApiException() + { + var expectedResponse = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent("Invalid JSON") + }; + + mockHandler + .Expect(HttpMethod.Get, "http://api/GetApiResponseTestObject") + .Respond(req => expectedResponse); + + using var response = await fixture.GetApiResponseTestObject(); + await response.EnsureSuccessStatusCodeAsync(); + + Assert.True(response.IsSuccessStatusCode); + Assert.False(response.IsSuccessful); + Assert.NotNull(response.Error); + } + + /// + /// Test to verify if EnsureSuccessfulAsync throws an ApiException if we have a success status code, but there is a deserialization error + /// + [Fact] + public async Task When_SerializationErrorOnSuccessStatusCode_EnsureSuccessfulAsync_ThrowsApiException() + { + var expectedResponse = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent("Invalid JSON") + }; + + mockHandler + .Expect(HttpMethod.Get, "http://api/GetApiResponseTestObject") + .Respond(req => expectedResponse); + + using var response = await fixture.GetApiResponseTestObject(); + var actualException = await Assert.ThrowsAsync( + () => response.EnsureSuccessfulAsync() + ); + + Assert.True(response.IsSuccessStatusCode); + Assert.False(response.IsSuccessful); + Assert.NotNull(actualException); + Assert.IsType(actualException.InnerException); + } + [Fact] public async Task WhenProblemDetailsResponseContainsExtensions_ShouldHydrateExtensions() { diff --git a/Refit/ApiResponse.cs b/Refit/ApiResponse.cs index 2867b8147..4affdd5bc 100644 --- a/Refit/ApiResponse.cs +++ b/Refit/ApiResponse.cs @@ -71,12 +71,21 @@ public sealed class ApiResponse( /// Indicates whether the request was successful. /// #if NET6_0_OR_GREATER - [MemberNotNullWhen(true, nameof(Content))] [MemberNotNullWhen(true, nameof(ContentHeaders))] [MemberNotNullWhen(false, nameof(Error))] #endif public bool IsSuccessStatusCode => response.IsSuccessStatusCode; + /// + /// Indicates whether the request was successful and there wasn't any other error (for example, during content deserialization). + /// +#if NET6_0_OR_GREATER + [MemberNotNullWhen(true, nameof(Content))] + [MemberNotNullWhen(true, nameof(ContentHeaders))] + [MemberNotNullWhen(false, nameof(Error))] +#endif + public bool IsSuccessful => IsSuccessStatusCode && Error is null; + /// /// The reason phrase which typically is sent by the server together with the status code. /// @@ -119,20 +128,22 @@ public async Task> EnsureSuccessStatusCodeAsync() { if (!IsSuccessStatusCode) { - var exception = - Error - ?? await ApiException - .Create( - response.RequestMessage!, - response.RequestMessage!.Method, - response, - Settings - ) - .ConfigureAwait(false); + await ThrowsApiExceptionAsync().ConfigureAwait(false); + } - Dispose(); + return this; + } - throw exception; + /// + /// Ensures the request was successful and without any other error by throwing an exception in case of failure + /// + /// The current + /// + public async Task> EnsureSuccessfulAsync() + { + if (!IsSuccessful) + { + await ThrowsApiExceptionAsync().ConfigureAwait(false); } return this; @@ -147,6 +158,24 @@ void Dispose(bool disposing) response.Dispose(); } + + private async Task ThrowsApiExceptionAsync() + { + var exception = + Error + ?? await ApiException + .Create( + response.RequestMessage!, + response.RequestMessage!.Method, + response, + Settings + ) + .ConfigureAwait(false); + + Dispose(); + + throw exception; + } } /// @@ -171,10 +200,17 @@ public interface IApiResponse : IApiResponse /// /// Indicates whether the request was successful. /// - [MemberNotNullWhen(true, nameof(Content))] [MemberNotNullWhen(true, nameof(ContentHeaders))] [MemberNotNullWhen(false, nameof(Error))] new bool IsSuccessStatusCode { get; } + + /// + /// Indicates whether the request was successful and there wasn't any other error (for example, during content deserialization). + /// + [MemberNotNullWhen(true, nameof(Content))] + [MemberNotNullWhen(true, nameof(ContentHeaders))] + [MemberNotNullWhen(false, nameof(Error))] + new bool IsSuccessful { get; } #endif /// @@ -207,6 +243,15 @@ public interface IApiResponse : IDisposable #endif bool IsSuccessStatusCode { get; } + /// + /// Indicates whether the request was successful and there wasn't any other error (for example, during content deserialization). + /// +#if NET6_0_OR_GREATER + [MemberNotNullWhen(true, nameof(ContentHeaders))] + [MemberNotNullWhen(false, nameof(Error))] +#endif + bool IsSuccessful { get; } + /// /// HTTP response status code. ///