Skip to content

Commit ebab32c

Browse files
Create ApiRequestException for wrapping SendAsync exceptions (#2052)
* Create ApiRequestException for wrapping SendAsync exceptions * Remove commented line, fix spelling errors * chore: Update readme for V11 breaking change, update verified API approval tests, fix TargetFramework of Stub Generators - V10 has been released, so next major release is V11 - API Approval tests were failing due to the breaking changes - The .NET 10 SDK has issues parsing `TargetFrameworks` with just a single entry. Changed to `TargetFramework` (singular) * test: Update .NET 10 verified API approval tests * test: Update ApiApprovalTests --------- Co-authored-by: Chris Pulman <chris.pulman@yahoo.com>
1 parent 6ee5bb1 commit ebab32c

18 files changed

Lines changed: 854 additions & 188 deletions

InterfaceStubGenerator.Roslyn38/InterfaceStubGenerator.Roslyn38.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
4-
<TargetFrameworks>netstandard2.0</TargetFrameworks>
4+
<TargetFramework>netstandard2.0</TargetFramework>
55
<AssemblyName>InterfaceStubGeneratorV1</AssemblyName>
66
<RootNamespace>Refit.Generator</RootNamespace>
77
<IsPackable>false</IsPackable>

InterfaceStubGenerator.Roslyn41/InterfaceStubGenerator.Roslyn41.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
4-
<TargetFrameworks>netstandard2.0</TargetFrameworks>
4+
<TargetFramework>netstandard2.0</TargetFramework>
55
<AssemblyName>InterfaceStubGeneratorV2</AssemblyName>
66
<RootNamespace>Refit.Generator</RootNamespace>
77
<IsPackable>false</IsPackable>

InterfaceStubGenerator.Roslyn50/InterfaceStubGenerator.Roslyn50.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
4-
<TargetFrameworks>netstandard2.0</TargetFrameworks>
4+
<TargetFramework>netstandard2.0</TargetFramework>
55
<AssemblyName>InterfaceStubGeneratorV3</AssemblyName>
66
<RootNamespace>Refit.Generator</RootNamespace>
77
<IsPackable>false</IsPackable>

README.md

Lines changed: 61 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ services
3838

3939
* [Where does this work?](#where-does-this-work)
4040
* [Breaking changes in 6.x](#breaking-changes-in-6x)
41+
* [Breaking changes in 11.x](#breaking-changes-in-11x)
4142
* [API Attributes](#api-attributes)
4243
* [Querystrings](#querystrings)
4344
* [Dynamic Querystring Parameters](#dynamic-querystring-parameters)
@@ -112,6 +113,24 @@ Refit 6.3 splits out the XML serialization via `XmlContentSerializer` into a sep
112113
is to reduce the dependency size when using Refit with Web Assembly (WASM) applications. If you require XML, add a reference
113114
to `Refit.Xml`.
114115

116+
### V11.x.x
117+
118+
#### Breaking changes in 11.x
119+
120+
Refit 10 introduces `ApiRequestException` to represent requests that fail before receiving a response from the server.
121+
This exception will now wrap previous exceptions such as `HttpRequestException` and `TaskCanceledException` when they occur during request execution.
122+
123+
* If you were not wrapping responses with `IApiResponse` and were catching these exceptions directly, you will need to update your code to catch `ApiRequestException` instead.
124+
* If you were wrapping responses with `IApiResponse`, these exceptions will no longer be thrown and will instead be captured in the `IApiResponse.Error` property.
125+
You can use the new `IApiResponse.HasRequestError(out var apiRequestException)` method to safely check and retrieve the `ApiRequestException` instance.
126+
127+
The `IApiResponse.Error` property's type has also changed to `ApiExceptionBase`, which is the new base class for `ApiException` and `ApiRequestException`.
128+
If your code accessed members specific to `ApiException` (i.e. anything related to the response from the server), you can use the new `IApiResponse.HasResponseError(out var apiException)` method to safely check and retrieve the `ApiException` instance.
129+
130+
All response-related properties of `IApiResponse` are now nullable.
131+
The new `IApiResponse.IsReceived` property can be used to check if a response was received from the server, and will mark those properties as non-null.
132+
The original `IApiResponse.IsSuccessful` and `IApiResponse.IsSuccessStatusCode` properties can still be used to check if the response was received and is successful.
133+
115134
### API Attributes
116135

117136
Every method must have an HTTP attribute that provides the request method and
@@ -1110,31 +1129,34 @@ Task<ApiResponse<User>> GetUser(string user);
11101129
//Calling the API
11111130
var response = await gitHubApi.GetUser("octocat");
11121131

1113-
//Getting the status code (returns a value from the System.Net.HttpStatusCode enumeration)
1114-
var httpStatus = response.StatusCode;
1115-
11161132
//Determining if a success status code was received and there wasn't any other error
11171133
//(for example, during content deserialization)
11181134
if(response.IsSuccessful)
11191135
{
11201136
//YAY! Do the thing...
11211137
}
11221138

1123-
//Retrieving a well-known header value (e.g. "Server" header)
1124-
var serverHeaderValue = response.Headers.Server != null ? response.Headers.Server.ToString() : string.Empty;
1139+
if (response.IsReceived)
1140+
{
1141+
//Getting the status code (returns a value from the System.Net.HttpStatusCode enumeration)
1142+
var httpStatus = response.StatusCode;
11251143

1126-
//Retrieving a custom header value
1127-
var customHeaderValue = string.Join(',', response.Headers.GetValues("A-Custom-Header"));
1144+
//Retrieving a well-known header value (e.g. "Server" header)
1145+
var serverHeaderValue = response.Headers.Server != null ? response.Headers.Server.ToString() : string.Empty;
11281146

1129-
//Looping through all the headers
1130-
foreach(var header in response.Headers)
1131-
{
1132-
var headerName = header.Key;
1133-
var headerValue = string.Join(',', header.Value);
1134-
}
1147+
//Retrieving a custom header value
1148+
var customHeaderValue = string.Join(',', response.Headers.GetValues("A-Custom-Header"));
11351149

1136-
//Finally, retrieving the content in the response body as a strongly-typed object
1137-
var user = response.Content;
1150+
//Looping through all the headers
1151+
foreach(var header in response.Headers)
1152+
{
1153+
var headerName = header.Key;
1154+
var headerValue = string.Join(',', header.Value);
1155+
}
1156+
1157+
//Finally, retrieving the content in the response body as a strongly-typed object
1158+
var user = response.Content;
1159+
}
11381160
```
11391161

11401162
### Using generic interfaces
@@ -1402,7 +1424,9 @@ Refit also ships analyzers for newer Roslyn toolchains, including a Roslyn 5.0 b
14021424
Refit has different exception handling behavior depending on if your Refit interface methods return `Task<T>` or if they return `Task<IApiResponse>`, `Task<IApiResponse<T>>`, or `Task<ApiResponse<T>>`.
14031425

14041426
#### <a id="when-returning-taskapiresponset"></a>When returning `Task<IApiResponse>`, `Task<IApiResponse<T>>`, or `Task<ApiResponse<T>>`
1405-
Refit traps any `ApiException` raised by the `ExceptionFactory` when processing the response, and any errors that occur when attempting to deserialize the response to `ApiResponse<T>`, and populates the exception into the `Error` property on `ApiResponse<T>` without throwing the exception.
1427+
Refit traps any `HttpRequestException` or `TaskCanceledException` raised by the `HttpClient` in an `ApiRequestException`.
1428+
Refit also traps any `ApiException` raised by the `ExceptionFactory` when processing the response, and any errors that occur when attempting to deserialize the response to `ApiResponse<T>`.
1429+
In both cases, it will populate the exception into the `Error` property on `ApiResponse<T>` without throwing the exception.
14061430

14071431
You can then decide what to do like so:
14081432

@@ -1414,25 +1438,42 @@ if(response.IsSuccessful)
14141438
}
14151439
else
14161440
{
1417-
_logger.LogError(response.Error, response.Error.Content);
1441+
// If you want to distinguish between request and response errors
1442+
if (response.HasRequestError(out var requestError))
1443+
_logger.LogError(requestError, "An error occurred while sending the request.");
1444+
else if (response.HasResponseError(out var responseError))
1445+
_logger.LogError(responseError, responseError.Content);
1446+
1447+
// Or just log the error directly
1448+
_logger.LogError(response.Error, "An error occurred while calling the API.");
14181449
}
14191450
```
14201451

14211452
> [!NOTE]
14221453
> 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.
14231454
14241455
#### When returning `Task<T>`
1425-
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>`.
1456+
Refit throws any exception raised by the `HttpClient` and wraps it in an `ApiRequestException`.
1457+
It also 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>`.
14261458

14271459
```csharp
14281460
// ...
14291461
try
14301462
{
14311463
var result = await awesomeApi.GetFooAsync("bar");
14321464
}
1465+
catch (ApiRequestException exception)
1466+
{
1467+
//exception handling for when a response was not received from the server
1468+
}
14331469
catch (ApiException exception)
14341470
{
1435-
//exception handling
1471+
//exception handling for when a response was received from the server
1472+
}
1473+
// Or to not distinguish between request/response exceptions
1474+
catch (ApiExceptionBase exception)
1475+
{
1476+
//exception handling for when an error occurs during the request/response
14361477
}
14371478
// ...
14381479
```
@@ -1464,7 +1505,7 @@ catch (ApiException exception)
14641505

14651506
#### Providing a custom `ExceptionFactory`
14661507

1467-
You can also override default exceptions behavior that are raised by the `ExceptionFactory` when processing the result by providing a custom exception factory in `RefitSettings`. For example, you can suppress all exceptions with the following:
1508+
You can also override default exceptions behavior that are raised by the `ExceptionFactory` when processing the result by providing a custom exception factory in `RefitSettings`. For example, you can suppress all `ApiException`s with the following:
14681509

14691510
```csharp
14701511
var nullTask = Task.FromResult<Exception>(null);

0 commit comments

Comments
 (0)