Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add IsSuccessful property to ApiResponse #1891

Merged
merged 13 commits into from
Oct 29, 2024
Merged
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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...
}
Expand Down Expand Up @@ -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
}
Expand All @@ -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<T>`
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<T>`.

Expand Down
20 changes: 18 additions & 2 deletions Refit.Tests/API/ApiApprovalTests.Refit.DotNet6_0.verified.txt
Original file line number Diff line number Diff line change
Expand Up @@ -40,19 +40,25 @@ 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; }
public System.Net.HttpStatusCode StatusCode { get; }
public System.Version Version { get; }
public void Dispose() { }
public System.Threading.Tasks.Task<Refit.ApiResponse<T>> EnsureSuccessStatusCodeAsync() { }
public System.Threading.Tasks.Task<Refit.ApiResponse<T>> EnsureSuccessfulAsync() { }
}
[System.AttributeUsage(System.AttributeTargets.Property | System.AttributeTargets.Parameter)]
[System.Obsolete("Use Refit.StreamPart, Refit.ByteArrayPart, Refit.FileInfoPart or if necessary, in" +
Expand Down Expand Up @@ -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; }
Expand All @@ -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
{
Expand Down
20 changes: 18 additions & 2 deletions Refit.Tests/API/ApiApprovalTests.Refit.DotNet8_0.verified.txt
Original file line number Diff line number Diff line change
Expand Up @@ -40,19 +40,25 @@ 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; }
public System.Net.HttpStatusCode StatusCode { get; }
public System.Version Version { get; }
public void Dispose() { }
public System.Threading.Tasks.Task<Refit.ApiResponse<T>> EnsureSuccessStatusCodeAsync() { }
public System.Threading.Tasks.Task<Refit.ApiResponse<T>> EnsureSuccessfulAsync() { }
}
[System.AttributeUsage(System.AttributeTargets.Property | System.AttributeTargets.Parameter)]
[System.Obsolete("Use Refit.StreamPart, Refit.ByteArrayPart, Refit.FileInfoPart or if necessary, in" +
Expand Down Expand Up @@ -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; }
Expand All @@ -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
{
Expand Down
71 changes: 71 additions & 0 deletions Refit.Tests/ResponseTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,77 @@ public async Task When_BadRequest_EnsureSuccessStatusCodeAsync_ThrowsValidationE
Assert.Equal("type", actualException.Content.Type);
}

/// <summary>
/// Test to verify if IsSuccessful returns false if we have a success status code, but there is a deserialization error
/// </summary>
[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);
}

/// <summary>
/// Test to verify if EnsureSuccessStatusCodeAsync do not throw an ApiException if we have a success status code, but there is a deserialization error
/// </summary>
[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);
}

/// <summary>
/// Test to verify if EnsureSuccessfulAsync throws an ApiException if we have a success status code, but there is a deserialization error
/// </summary>
[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<ApiException>(
() => response.EnsureSuccessfulAsync()
);

Assert.True(response.IsSuccessStatusCode);
Assert.False(response.IsSuccessful);
Assert.NotNull(actualException);
Assert.IsType<System.Text.Json.JsonException>(actualException.InnerException);
}

[Fact]
public async Task WhenProblemDetailsResponseContainsExtensions_ShouldHydrateExtensions()
{
Expand Down
73 changes: 59 additions & 14 deletions Refit/ApiResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,21 @@ public sealed class ApiResponse<T>(
/// Indicates whether the request was successful.
/// </summary>
#if NET6_0_OR_GREATER
[MemberNotNullWhen(true, nameof(Content))]
[MemberNotNullWhen(true, nameof(ContentHeaders))]
[MemberNotNullWhen(false, nameof(Error))]
#endif
public bool IsSuccessStatusCode => response.IsSuccessStatusCode;

/// <summary>
/// Indicates whether the request was successful and there wasn't any other error (for example, during content deserialization).
/// </summary>
#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;

/// <summary>
/// The reason phrase which typically is sent by the server together with the status code.
/// </summary>
Expand Down Expand Up @@ -119,20 +128,22 @@ public async Task<ApiResponse<T>> 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;
/// <summary>
/// Ensures the request was successful and without any other error by throwing an exception in case of failure
/// </summary>
/// <returns>The current <see cref="ApiResponse{T}"/></returns>
/// <exception cref="ApiException"></exception>
public async Task<ApiResponse<T>> EnsureSuccessfulAsync()
{
if (!IsSuccessful)
{
await ThrowsApiExceptionAsync().ConfigureAwait(false);
}

return this;
Expand All @@ -147,6 +158,24 @@ void Dispose(bool disposing)

response.Dispose();
}

private async Task<ApiException> ThrowsApiExceptionAsync()
{
var exception =
Error
?? await ApiException
.Create(
response.RequestMessage!,
response.RequestMessage!.Method,
response,
Settings
)
.ConfigureAwait(false);

Dispose();

throw exception;
}
}

/// <inheritdoc/>
Expand All @@ -171,10 +200,17 @@ public interface IApiResponse<out T> : IApiResponse
/// <summary>
/// Indicates whether the request was successful.
/// </summary>
[MemberNotNullWhen(true, nameof(Content))]
[MemberNotNullWhen(true, nameof(ContentHeaders))]
[MemberNotNullWhen(false, nameof(Error))]
new bool IsSuccessStatusCode { get; }

/// <summary>
/// Indicates whether the request was successful and there wasn't any other error (for example, during content deserialization).
/// </summary>
[MemberNotNullWhen(true, nameof(Content))]
[MemberNotNullWhen(true, nameof(ContentHeaders))]
[MemberNotNullWhen(false, nameof(Error))]
new bool IsSuccessful { get; }
#endif

/// <summary>
Expand Down Expand Up @@ -207,6 +243,15 @@ public interface IApiResponse : IDisposable
#endif
bool IsSuccessStatusCode { get; }

/// <summary>
/// Indicates whether the request was successful and there wasn't any other error (for example, during content deserialization).
/// </summary>
#if NET6_0_OR_GREATER
[MemberNotNullWhen(true, nameof(ContentHeaders))]
[MemberNotNullWhen(false, nameof(Error))]
#endif
bool IsSuccessful { get; }

/// <summary>
/// HTTP response status code.
/// </summary>
Expand Down
Loading