Skip to content

Commit dc74700

Browse files
Add IsSuccessful property to ApiResponse (#1891)
* Use Error property when throwing exception #1376 * Add Unit test * Update test assertions * Add IsSuccess property and EnsureSuccessAsync method Introduce IsSuccess property to ApiResponse<T> and related interfaces to indicate request success. Add EnsureSuccessAsync method to handle errors, including deserialization issues. Refactor EnsureSuccessStatusCodeAsync to use ThrowsApiExceptionAsync. Update Dispose method for exception handling. Add unit tests to verify new behavior. * Refactor API response success properties and methods Renamed properties and methods related to the success status of an API response in the `Refit` library: - `IsSuccess` to `IsSuccessful` - `EnsureSuccessAsync` to `EnsureSuccessfulAsync` * Update IsSuccessful usage and documentation * Fix a typo * Update tests Added a new test method `When_SerializationErrorOnSuccessStatusCode_EnsureSuccesStatusCodeAsync_DoNotThrowApiException` to verify that `EnsureSuccessStatusCodeAsync` does not throw an `ApiException` on deserialization error with a success status code. Included assertions to check response status and error presence. Enhanced existing test method `When_SerializationErrorOnSuccessStatusCode_EnsureSuccessfulAsync_ThrowsApiException` with additional assertions. --------- Co-authored-by: Chris Pulman <chris.pulman@yahoo.com>
1 parent 2d2169c commit dc74700

5 files changed

Lines changed: 173 additions & 21 deletions

File tree

README.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1099,8 +1099,9 @@ var response = await gitHubApi.GetUser("octocat");
10991099
//Getting the status code (returns a value from the System.Net.HttpStatusCode enumeration)
11001100
var httpStatus = response.StatusCode;
11011101

1102-
//Determining if a success status code was received
1103-
if(response.IsSuccessStatusCode)
1102+
//Determining if a success status code was received and there wasn't any other error
1103+
//(for example, during content deserialization)
1104+
if(response.IsSuccessful)
11041105
{
11051106
//YAY! Do the thing...
11061107
}
@@ -1362,7 +1363,7 @@ You can then decide what to do like so:
13621363

13631364
```csharp
13641365
var response = await _myRefitClient.GetSomeStuff();
1365-
if(response.IsSuccessStatusCode)
1366+
if(response.IsSuccessful)
13661367
{
13671368
//do your thing
13681369
}
@@ -1372,6 +1373,9 @@ else
13721373
}
13731374
```
13741375

1376+
> [!NOTE]
1377+
> 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.
1378+
13751379
#### When returning `Task<T>`
13761380
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>`.
13771381

Refit.Tests/API/ApiApprovalTests.Refit.DotNet6_0.verified.txt

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,19 +40,25 @@ namespace Refit
4040
public Refit.ApiException? Error { get; }
4141
public System.Net.Http.Headers.HttpResponseHeaders Headers { get; }
4242
[System.Diagnostics.CodeAnalysis.MemberNotNullWhen(false, "Error")]
43+
[System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "ContentHeaders")]
44+
[get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(false, "Error")]
45+
[get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "ContentHeaders")]
46+
public bool IsSuccessStatusCode { get; }
47+
[System.Diagnostics.CodeAnalysis.MemberNotNullWhen(false, "Error")]
4348
[System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "Content")]
4449
[System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "ContentHeaders")]
4550
[get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(false, "Error")]
4651
[get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "Content")]
4752
[get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "ContentHeaders")]
48-
public bool IsSuccessStatusCode { get; }
53+
public bool IsSuccessful { get; }
4954
public string? ReasonPhrase { get; }
5055
public System.Net.Http.HttpRequestMessage? RequestMessage { get; }
5156
public Refit.RefitSettings Settings { get; }
5257
public System.Net.HttpStatusCode StatusCode { get; }
5358
public System.Version Version { get; }
5459
public void Dispose() { }
5560
public System.Threading.Tasks.Task<Refit.ApiResponse<T>> EnsureSuccessStatusCodeAsync() { }
61+
public System.Threading.Tasks.Task<Refit.ApiResponse<T>> EnsureSuccessfulAsync() { }
5662
}
5763
[System.AttributeUsage(System.AttributeTargets.Property | System.AttributeTargets.Parameter)]
5864
[System.Obsolete("Use Refit.StreamPart, Refit.ByteArrayPart, Refit.FileInfoPart or if necessary, in" +
@@ -190,6 +196,11 @@ namespace Refit
190196
[get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(false, "Error")]
191197
[get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "ContentHeaders")]
192198
bool IsSuccessStatusCode { get; }
199+
[System.Diagnostics.CodeAnalysis.MemberNotNullWhen(false, "Error")]
200+
[System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "ContentHeaders")]
201+
[get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(false, "Error")]
202+
[get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "ContentHeaders")]
203+
bool IsSuccessful { get; }
193204
string? ReasonPhrase { get; }
194205
System.Net.Http.HttpRequestMessage? RequestMessage { get; }
195206
System.Net.HttpStatusCode StatusCode { get; }
@@ -201,12 +212,17 @@ namespace Refit
201212
new System.Net.Http.Headers.HttpContentHeaders? ContentHeaders { get; }
202213
new Refit.ApiException? Error { get; }
203214
[System.Diagnostics.CodeAnalysis.MemberNotNullWhen(false, "Error")]
215+
[System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "ContentHeaders")]
216+
[get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(false, "Error")]
217+
[get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "ContentHeaders")]
218+
new bool IsSuccessStatusCode { get; }
219+
[System.Diagnostics.CodeAnalysis.MemberNotNullWhen(false, "Error")]
204220
[System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "Content")]
205221
[System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "ContentHeaders")]
206222
[get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(false, "Error")]
207223
[get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "Content")]
208224
[get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "ContentHeaders")]
209-
new bool IsSuccessStatusCode { get; }
225+
new bool IsSuccessful { get; }
210226
}
211227
public interface IFormUrlEncodedParameterFormatter
212228
{

Refit.Tests/API/ApiApprovalTests.Refit.DotNet8_0.verified.txt

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,19 +40,25 @@ namespace Refit
4040
public Refit.ApiException? Error { get; }
4141
public System.Net.Http.Headers.HttpResponseHeaders Headers { get; }
4242
[System.Diagnostics.CodeAnalysis.MemberNotNullWhen(false, "Error")]
43+
[System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "ContentHeaders")]
44+
[get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(false, "Error")]
45+
[get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "ContentHeaders")]
46+
public bool IsSuccessStatusCode { get; }
47+
[System.Diagnostics.CodeAnalysis.MemberNotNullWhen(false, "Error")]
4348
[System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "Content")]
4449
[System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "ContentHeaders")]
4550
[get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(false, "Error")]
4651
[get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "Content")]
4752
[get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "ContentHeaders")]
48-
public bool IsSuccessStatusCode { get; }
53+
public bool IsSuccessful { get; }
4954
public string? ReasonPhrase { get; }
5055
public System.Net.Http.HttpRequestMessage? RequestMessage { get; }
5156
public Refit.RefitSettings Settings { get; }
5257
public System.Net.HttpStatusCode StatusCode { get; }
5358
public System.Version Version { get; }
5459
public void Dispose() { }
5560
public System.Threading.Tasks.Task<Refit.ApiResponse<T>> EnsureSuccessStatusCodeAsync() { }
61+
public System.Threading.Tasks.Task<Refit.ApiResponse<T>> EnsureSuccessfulAsync() { }
5662
}
5763
[System.AttributeUsage(System.AttributeTargets.Property | System.AttributeTargets.Parameter)]
5864
[System.Obsolete("Use Refit.StreamPart, Refit.ByteArrayPart, Refit.FileInfoPart or if necessary, in" +
@@ -190,6 +196,11 @@ namespace Refit
190196
[get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(false, "Error")]
191197
[get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "ContentHeaders")]
192198
bool IsSuccessStatusCode { get; }
199+
[System.Diagnostics.CodeAnalysis.MemberNotNullWhen(false, "Error")]
200+
[System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "ContentHeaders")]
201+
[get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(false, "Error")]
202+
[get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "ContentHeaders")]
203+
bool IsSuccessful { get; }
193204
string? ReasonPhrase { get; }
194205
System.Net.Http.HttpRequestMessage? RequestMessage { get; }
195206
System.Net.HttpStatusCode StatusCode { get; }
@@ -201,12 +212,17 @@ namespace Refit
201212
new System.Net.Http.Headers.HttpContentHeaders? ContentHeaders { get; }
202213
new Refit.ApiException? Error { get; }
203214
[System.Diagnostics.CodeAnalysis.MemberNotNullWhen(false, "Error")]
215+
[System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "ContentHeaders")]
216+
[get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(false, "Error")]
217+
[get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "ContentHeaders")]
218+
new bool IsSuccessStatusCode { get; }
219+
[System.Diagnostics.CodeAnalysis.MemberNotNullWhen(false, "Error")]
204220
[System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "Content")]
205221
[System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "ContentHeaders")]
206222
[get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(false, "Error")]
207223
[get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "Content")]
208224
[get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "ContentHeaders")]
209-
new bool IsSuccessStatusCode { get; }
225+
new bool IsSuccessful { get; }
210226
}
211227
public interface IFormUrlEncodedParameterFormatter
212228
{

Refit.Tests/ResponseTests.cs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,77 @@ public async Task When_BadRequest_EnsureSuccessStatusCodeAsync_ThrowsValidationE
174174
Assert.Equal("type", actualException.Content.Type);
175175
}
176176

177+
/// <summary>
178+
/// Test to verify if IsSuccessful returns false if we have a success status code, but there is a deserialization error
179+
/// </summary>
180+
[Fact]
181+
public async Task When_SerializationErrorOnSuccessStatusCode_IsSuccessful_ShouldReturnFalse()
182+
{
183+
var expectedResponse = new HttpResponseMessage(HttpStatusCode.OK)
184+
{
185+
Content = new StringContent("Invalid JSON")
186+
};
187+
188+
mockHandler
189+
.Expect(HttpMethod.Get, "http://api/GetApiResponseTestObject")
190+
.Respond(req => expectedResponse);
191+
192+
using var response = await fixture.GetApiResponseTestObject();
193+
194+
Assert.True(response.IsSuccessStatusCode);
195+
Assert.False(response.IsSuccessful);
196+
Assert.NotNull(response.Error);
197+
}
198+
199+
/// <summary>
200+
/// Test to verify if EnsureSuccessStatusCodeAsync do not throw an ApiException if we have a success status code, but there is a deserialization error
201+
/// </summary>
202+
[Fact]
203+
public async Task When_SerializationErrorOnSuccessStatusCode_EnsureSuccesStatusCodeAsync_DoNotThrowApiException()
204+
{
205+
var expectedResponse = new HttpResponseMessage(HttpStatusCode.OK)
206+
{
207+
Content = new StringContent("Invalid JSON")
208+
};
209+
210+
mockHandler
211+
.Expect(HttpMethod.Get, "http://api/GetApiResponseTestObject")
212+
.Respond(req => expectedResponse);
213+
214+
using var response = await fixture.GetApiResponseTestObject();
215+
await response.EnsureSuccessStatusCodeAsync();
216+
217+
Assert.True(response.IsSuccessStatusCode);
218+
Assert.False(response.IsSuccessful);
219+
Assert.NotNull(response.Error);
220+
}
221+
222+
/// <summary>
223+
/// Test to verify if EnsureSuccessfulAsync throws an ApiException if we have a success status code, but there is a deserialization error
224+
/// </summary>
225+
[Fact]
226+
public async Task When_SerializationErrorOnSuccessStatusCode_EnsureSuccessfulAsync_ThrowsApiException()
227+
{
228+
var expectedResponse = new HttpResponseMessage(HttpStatusCode.OK)
229+
{
230+
Content = new StringContent("Invalid JSON")
231+
};
232+
233+
mockHandler
234+
.Expect(HttpMethod.Get, "http://api/GetApiResponseTestObject")
235+
.Respond(req => expectedResponse);
236+
237+
using var response = await fixture.GetApiResponseTestObject();
238+
var actualException = await Assert.ThrowsAsync<ApiException>(
239+
() => response.EnsureSuccessfulAsync()
240+
);
241+
242+
Assert.True(response.IsSuccessStatusCode);
243+
Assert.False(response.IsSuccessful);
244+
Assert.NotNull(actualException);
245+
Assert.IsType<System.Text.Json.JsonException>(actualException.InnerException);
246+
}
247+
177248
[Fact]
178249
public async Task WhenProblemDetailsResponseContainsExtensions_ShouldHydrateExtensions()
179250
{

Refit/ApiResponse.cs

Lines changed: 59 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -71,12 +71,21 @@ public sealed class ApiResponse<T>(
7171
/// Indicates whether the request was successful.
7272
/// </summary>
7373
#if NET6_0_OR_GREATER
74-
[MemberNotNullWhen(true, nameof(Content))]
7574
[MemberNotNullWhen(true, nameof(ContentHeaders))]
7675
[MemberNotNullWhen(false, nameof(Error))]
7776
#endif
7877
public bool IsSuccessStatusCode => response.IsSuccessStatusCode;
7978

79+
/// <summary>
80+
/// Indicates whether the request was successful and there wasn't any other error (for example, during content deserialization).
81+
/// </summary>
82+
#if NET6_0_OR_GREATER
83+
[MemberNotNullWhen(true, nameof(Content))]
84+
[MemberNotNullWhen(true, nameof(ContentHeaders))]
85+
[MemberNotNullWhen(false, nameof(Error))]
86+
#endif
87+
public bool IsSuccessful => IsSuccessStatusCode && Error is null;
88+
8089
/// <summary>
8190
/// The reason phrase which typically is sent by the server together with the status code.
8291
/// </summary>
@@ -119,20 +128,22 @@ public async Task<ApiResponse<T>> EnsureSuccessStatusCodeAsync()
119128
{
120129
if (!IsSuccessStatusCode)
121130
{
122-
var exception =
123-
Error
124-
?? await ApiException
125-
.Create(
126-
response.RequestMessage!,
127-
response.RequestMessage!.Method,
128-
response,
129-
Settings
130-
)
131-
.ConfigureAwait(false);
131+
await ThrowsApiExceptionAsync().ConfigureAwait(false);
132+
}
132133

133-
Dispose();
134+
return this;
135+
}
134136

135-
throw exception;
137+
/// <summary>
138+
/// Ensures the request was successful and without any other error by throwing an exception in case of failure
139+
/// </summary>
140+
/// <returns>The current <see cref="ApiResponse{T}"/></returns>
141+
/// <exception cref="ApiException"></exception>
142+
public async Task<ApiResponse<T>> EnsureSuccessfulAsync()
143+
{
144+
if (!IsSuccessful)
145+
{
146+
await ThrowsApiExceptionAsync().ConfigureAwait(false);
136147
}
137148

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

148159
response.Dispose();
149160
}
161+
162+
private async Task<ApiException> ThrowsApiExceptionAsync()
163+
{
164+
var exception =
165+
Error
166+
?? await ApiException
167+
.Create(
168+
response.RequestMessage!,
169+
response.RequestMessage!.Method,
170+
response,
171+
Settings
172+
)
173+
.ConfigureAwait(false);
174+
175+
Dispose();
176+
177+
throw exception;
178+
}
150179
}
151180

152181
/// <inheritdoc/>
@@ -171,10 +200,17 @@ public interface IApiResponse<out T> : IApiResponse
171200
/// <summary>
172201
/// Indicates whether the request was successful.
173202
/// </summary>
174-
[MemberNotNullWhen(true, nameof(Content))]
175203
[MemberNotNullWhen(true, nameof(ContentHeaders))]
176204
[MemberNotNullWhen(false, nameof(Error))]
177205
new bool IsSuccessStatusCode { get; }
206+
207+
/// <summary>
208+
/// Indicates whether the request was successful and there wasn't any other error (for example, during content deserialization).
209+
/// </summary>
210+
[MemberNotNullWhen(true, nameof(Content))]
211+
[MemberNotNullWhen(true, nameof(ContentHeaders))]
212+
[MemberNotNullWhen(false, nameof(Error))]
213+
new bool IsSuccessful { get; }
178214
#endif
179215

180216
/// <summary>
@@ -207,6 +243,15 @@ public interface IApiResponse : IDisposable
207243
#endif
208244
bool IsSuccessStatusCode { get; }
209245

246+
/// <summary>
247+
/// Indicates whether the request was successful and there wasn't any other error (for example, during content deserialization).
248+
/// </summary>
249+
#if NET6_0_OR_GREATER
250+
[MemberNotNullWhen(true, nameof(ContentHeaders))]
251+
[MemberNotNullWhen(false, nameof(Error))]
252+
#endif
253+
bool IsSuccessful { get; }
254+
210255
/// <summary>
211256
/// HTTP response status code.
212257
/// </summary>

0 commit comments

Comments
 (0)