Skip to content

Commit 871bf88

Browse files
committed
simplified & documented error-handling.
1 parent 0088db9 commit 871bf88

File tree

10 files changed

+146
-75
lines changed

10 files changed

+146
-75
lines changed

Client/ApiException.cs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,23 +13,27 @@ public class ApiException : Exception
1313
/// <summary>The HTTP status of the response.</summary>
1414
public HttpStatusCode Status { get; protected set; }
1515

16-
/// <summary>The HTTP response which caused the exception.</summary>
17-
public HttpResponseMessage Response { get; protected set; }
16+
/// <summary>The HTTP response which caused the exception. (This provides response body deserialization for further handling, but you should probably make sure <see cref="IResponse.RaiseErrors"/> is <c>false</c> before using it.)</summary>
17+
public IResponse Response { get; protected set; }
18+
19+
/// <summary>The HTTP response message which caused the exception.</summary>
20+
public HttpResponseMessage ResponseMessage { get; protected set; }
1821

1922

2023
/*********
2124
** Public methods
2225
*********/
2326
/// <summary>Construct an instance.</summary>
2427
/// <param name="response">The HTTP response which caused the exception.</param>
25-
/// <param name="status">The HTTP status of the response.</param>
28+
/// <param name="responseMessage">The HTTP response message which caused the exception.</param>
2629
/// <param name="message">The error message that explains the reason for the exception.</param>
2730
/// <param name="innerException">The exception that is the cause of the current exception (or <c>null</c> for no inner exception).</param>
28-
public ApiException(HttpResponseMessage response, HttpStatusCode status, string message, Exception innerException = null)
31+
public ApiException(IResponse response, HttpResponseMessage responseMessage, string message, Exception innerException = null)
2932
: base(message, innerException)
3033
{
3134
this.Response = response;
32-
this.Status = status;
35+
this.ResponseMessage = responseMessage;
36+
this.Status = responseMessage.StatusCode;
3337
}
3438
}
3539
}

Client/Client.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
<Compile Include="IClient.cs" />
5555
<Compile Include="IFactory.cs" />
5656
<Compile Include="IRequest.cs" />
57+
<Compile Include="IResponse.cs" />
5758
<Compile Include="Properties\AssemblyInfo.cs" />
5859
</ItemGroup>
5960
<ItemGroup />

Client/Default/Request.cs

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
namespace Pathoschild.Http.Client.Default
1414
{
15-
/// <summary>Builds and dispatches an asynchronous HTTP request.</summary>
15+
/// <summary>Builds and dispatches an asynchronous HTTP request, and asynchronously parses the response.</summary>
1616
public class Request : IRequest
1717
{
1818
/*********
@@ -35,7 +35,7 @@ public class Request : IRequest
3535
public MediaTypeFormatterCollection Formatters { get; set; }
3636

3737
/// <summary>Whether to handle errors from the upstream server by throwing an exception.</summary>
38-
public bool ThrowError { get; set; }
38+
public bool RaiseErrors { get; set; }
3939

4040

4141
/*********
@@ -52,7 +52,7 @@ public Request(HttpRequestMessage message, MediaTypeFormatterCollection formatte
5252
this.Formatters = formatters;
5353
this.ResponseBuilder = dispatcher;
5454
this.Factory = factory ?? new Factory();
55-
this.ThrowError = true;
55+
this.RaiseErrors = true;
5656
}
5757

5858
/***
@@ -105,7 +105,7 @@ public virtual IRequest WithHeader(string key, string value)
105105
/// <returns>Returns the request builder for chaining.</returns>
106106
public virtual IRequest WithArgument(string key, object value)
107107
{
108-
return this.WithArguments(new Dictionary<string, object>(1) { {key, value} });
108+
return this.WithArguments(new Dictionary<string, object>(1) { { key, value } });
109109
}
110110

111111
/// <summary>Add HTTP query string arguments.</summary>
@@ -139,9 +139,9 @@ public virtual IRequest WithCustom(Action<HttpRequestMessage> request)
139139
***/
140140
/// <summary>Asynchronously retrieve the HTTP response.</summary>
141141
/// <exception cref="ApiException">An error occurred processing the response.</exception>
142-
public virtual Task<HttpResponseMessage> AsMessage()
142+
public virtual async Task<HttpResponseMessage> AsMessage()
143143
{
144-
return this.ValidateResponse(this.ResponseBuilder(this));
144+
return await this.ValidateResponse(this.ResponseBuilder(this)).ConfigureAwait(false);
145145
}
146146

147147
/// <summary>Asynchronously retrieve the response body as a deserialized model.</summary>
@@ -194,7 +194,7 @@ public virtual async Task<Stream> AsStream()
194194
** Synchronize
195195
***/
196196
/// <summary>Block the current thread until the asynchronous request completes. This method should only be called if you can't <c>await</c> instead, and may cause thread deadlocks in some circumstances (see https://github.com/Pathoschild/Pathoschild.FluentHttpClient#synchronous-use ).</summary>
197-
/// <exception cref="AggregateException">The HTTP response returned a non-success <see cref="HttpStatusCode"/> and <see cref="ThrowError"/> is <c>true</c>.</exception>
197+
/// <exception cref="AggregateException">The HTTP response returned a non-success <see cref="HttpStatusCode"/> and <see cref="RaiseErrors"/> is <c>true</c>.</exception>
198198
public void Wait()
199199
{
200200
this.AsMessage().Wait();
@@ -205,7 +205,7 @@ public void Wait()
205205
*********/
206206
/// <summary>Validate the HTTP response and raise any errors in the response as exceptions.</summary>
207207
/// <param name="request">The response message to validate.</param>
208-
/// <exception cref="ApiException">The HTTP response returned a non-success <see cref="HttpStatusCode"/> and <see cref="ThrowError"/> is <c>true</c>.</exception>
208+
/// <exception cref="ApiException">The HTTP response returned a non-success <see cref="HttpStatusCode"/> and <see cref="RaiseErrors"/> is <c>true</c>.</exception>
209209
protected async Task<HttpResponseMessage> ValidateResponse(Task<HttpResponseMessage> request)
210210
{
211211
// fetch request
@@ -216,11 +216,11 @@ protected async Task<HttpResponseMessage> ValidateResponse(Task<HttpResponseMess
216216

217217
/// <summary>Validate the HTTP response and raise any errors in the response as exceptions.</summary>
218218
/// <param name="message">The response message to validate.</param>
219-
/// <exception cref="ApiException">The HTTP response returned a non-success <see cref="HttpStatusCode"/> and <see cref="ThrowError"/> is <c>true</c>.</exception>
219+
/// <exception cref="ApiException">The HTTP response returned a non-success <see cref="HttpStatusCode"/> and <see cref="RaiseErrors"/> is <c>true</c>.</exception>
220220
protected virtual void ValidateResponse(HttpResponseMessage message)
221221
{
222-
if (this.ThrowError && !message.IsSuccessStatusCode)
223-
throw new ApiException(message, message.StatusCode, String.Format("The API query failed with status code {0}: {1}", message.StatusCode, message.ReasonPhrase));
222+
if (this.RaiseErrors && !message.IsSuccessStatusCode)
223+
throw new ApiException(this, message, String.Format("The API query failed with status code {0}: {1}", message.StatusCode, message.ReasonPhrase));
224224
}
225225

226226
/// <summary>Get the key=>value pairs represented by a dictionary or anonymous object.</summary>

Client/Delegating/DelegatingRequest.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,10 @@ public virtual MediaTypeFormatterCollection Formatters
3737
}
3838

3939
/// <summary>Whether to handle errors from the upstream server by throwing an exception.</summary>
40-
public bool ThrowError
40+
public bool RaiseErrors
4141
{
42-
get { return this.Implementation.ThrowError; }
43-
set { this.Implementation.ThrowError = value; }
42+
get { return this.Implementation.RaiseErrors; }
43+
set { this.Implementation.RaiseErrors = value; }
4444
}
4545

4646

@@ -174,7 +174,7 @@ public virtual Task<Stream> AsStream()
174174
** Synchronize
175175
***/
176176
/// <summary>Block the current thread until the asynchronous request completes.</summary>
177-
/// <exception cref="ApiException">The HTTP response returned a non-success <see cref="HttpStatusCode"/>, and <see cref="IRequest.ThrowError"/> is <c>true</c>.</exception>
177+
/// <exception cref="ApiException">The HTTP response returned a non-success <see cref="HttpStatusCode"/>, and <see cref="IResponse.RaiseErrors"/> is <c>true</c>.</exception>
178178
public void Wait()
179179
{
180180
this.Implementation.Wait();

Client/IRequest.cs

Lines changed: 2 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,20 @@
11
using System;
2-
using System.Collections.Generic;
3-
using System.IO;
42
using System.Net;
53
using System.Net.Http;
64
using System.Net.Http.Formatting;
75
using System.Net.Http.Headers;
8-
using System.Threading.Tasks;
96

107
namespace Pathoschild.Http.Client
118
{
12-
/// <summary>Builds and dispatches an asynchronous HTTP request.</summary>
13-
public interface IRequest
9+
/// <summary>Builds and dispatches an asynchronous HTTP request, and asynchronously parses the response.</summary>
10+
public interface IRequest : IResponse
1411
{
1512
/*********
1613
** Accessors
1714
*********/
1815
/// <summary>The underlying HTTP request message.</summary>
1916
HttpRequestMessage Message { get; set; }
2017

21-
/// <summary>The formatters used for serializing and deserializing message bodies.</summary>
22-
MediaTypeFormatterCollection Formatters { get; set; }
23-
24-
/// <summary>Whether to handle errors from the upstream server by throwing an exception.</summary>
25-
bool ThrowError { get; set; }
26-
2718

2819
/*********
2920
** Methods
@@ -73,38 +64,6 @@ public interface IRequest
7364
/// <returns>Returns the request builder for chaining.</returns>
7465
IRequest WithCustom(Action<HttpRequestMessage> request);
7566

76-
/***
77-
** Retrieve response
78-
***/
79-
/// <summary>Asynchronously retrieve the HTTP response.</summary>
80-
/// <exception cref="ApiException">An error occurred processing the response.</exception>
81-
Task<HttpResponseMessage> AsMessage();
82-
83-
/// <summary>Asynchronously retrieve the response body as a deserialized model.</summary>
84-
/// <typeparam name="T">The response model to deserialize into.</typeparam>
85-
/// <exception cref="ApiException">An error occurred processing the response.</exception>
86-
Task<T> As<T>();
87-
88-
/// <summary>Asynchronously retrieve the response body as a list of deserialized models.</summary>
89-
/// <typeparam name="T">The response model to deserialize into.</typeparam>
90-
/// <exception cref="ApiException">An error occurred processing the response.</exception>
91-
Task<List<T>> AsList<T>();
92-
93-
/// <summary>Asynchronously retrieve the response body as an array of <see cref="byte"/>.</summary>
94-
/// <returns>Returns the response body, or <c>null</c> if the response has no body.</returns>
95-
/// <exception cref="ApiException">An error occurred processing the response.</exception>
96-
Task<byte[]> AsByteArray();
97-
98-
/// <summary>Asynchronously retrieve the response body as a <see cref="string"/>.</summary>
99-
/// <returns>Returns the response body, or <c>null</c> if the response has no body.</returns>
100-
/// <exception cref="ApiException">An error occurred processing the response.</exception>
101-
Task<string> AsString();
102-
103-
/// <summary>Asynchronously retrieve the response body as a <see cref="Stream"/>.</summary>
104-
/// <returns>Returns the response body, or <c>null</c> if the response has no body.</returns>
105-
/// <exception cref="ApiException">An error occurred processing the response.</exception>
106-
Task<Stream> AsStream();
107-
10867
/***
10968
** Synchronize
11069
***/

Client/IResponse.cs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
using System.Collections.Generic;
2+
using System.IO;
3+
using System.Net.Http;
4+
using System.Net.Http.Formatting;
5+
using System.Threading.Tasks;
6+
7+
namespace Pathoschild.Http.Client
8+
{
9+
/// <summary>Asynchronously parses an HTTP response.</summary>
10+
public interface IResponse
11+
{
12+
/*********
13+
** Accessors
14+
*********/
15+
/// <summary>The formatters used for serializing and deserializing message bodies.</summary>
16+
MediaTypeFormatterCollection Formatters { get; set; }
17+
18+
/// <summary>Whether to handle errors from the upstream server by throwing an exception.</summary>
19+
bool RaiseErrors { get; set; }
20+
21+
22+
/*********
23+
** Methods
24+
*********/
25+
/// <summary>Asynchronously retrieve the HTTP response.</summary>
26+
/// <exception cref="ApiException">An error occurred processing the response.</exception>
27+
Task<HttpResponseMessage> AsMessage();
28+
29+
/// <summary>Asynchronously retrieve the response body as a deserialized model.</summary>
30+
/// <typeparam name="T">The response model to deserialize into.</typeparam>
31+
/// <exception cref="ApiException">An error occurred processing the response.</exception>
32+
Task<T> As<T>();
33+
34+
/// <summary>Asynchronously retrieve the response body as a list of deserialized models.</summary>
35+
/// <typeparam name="T">The response model to deserialize into.</typeparam>
36+
/// <exception cref="ApiException">An error occurred processing the response.</exception>
37+
Task<List<T>> AsList<T>();
38+
39+
/// <summary>Asynchronously retrieve the response body as an array of <see cref="byte"/>.</summary>
40+
/// <returns>Returns the response body, or <c>null</c> if the response has no body.</returns>
41+
/// <exception cref="ApiException">An error occurred processing the response.</exception>
42+
Task<byte[]> AsByteArray();
43+
44+
/// <summary>Asynchronously retrieve the response body as a <see cref="string"/>.</summary>
45+
/// <returns>Returns the response body, or <c>null</c> if the response has no body.</returns>
46+
/// <exception cref="ApiException">An error occurred processing the response.</exception>
47+
Task<string> AsString();
48+
49+
/// <summary>Asynchronously retrieve the response body as a <see cref="Stream"/>.</summary>
50+
/// <returns>Returns the response body, or <c>null</c> if the response has no body.</returns>
51+
/// <exception cref="ApiException">An error occurred processing the response.</exception>
52+
Task<Stream> AsStream();
53+
}
54+
}

Client/package.nuspec

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@
1414
- made all asynchronous methods async;
1515
- merged IRequestBuilder and IResponse into IRequest;
1616
- made IRequest awaitable;
17-
- eliminated the now-obsolete synchronous wrappers;
17+
- eliminated the now-obsolete synchronous wrappers.
1818
* Added default User-Agent header and auto-generated Accept headers.
1919
* Added support for anonymous object arguments: .WithArguments(new { id = 14, tenant = "example" }).
20+
* Simplified error-handling with new exception properties.
2021
</releaseNotes>
2122
<frameworkAssemblies>
2223
<frameworkAssembly assemblyName="System.Net.Http" />

README.md

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
**Pathoschild.FluentHttpClient** is a strongly-typed easy-to-use asynchronous REST API client, built on top of the .NET 4.5 [HttpClient][]. The client provides a single fluent interface that lets you create an HTTP request, dispatch and wait for it, and process the response. (The client will automatically inject any required HTTP configuration, like `User-Agent` and `Accept` headers.)
1+
**Pathoschild.FluentHttpClient** is a strongly-typed easy-to-use asynchronous REST API client, built on top of the .NET 4.5 [HttpClient][]. The client provides a single fluent interface that lets you create an HTTP request, dispatch and wait for it, and process the response. The client will automatically inject any required HTTP configuration (like `User-Agent` and `Accept` headers) and handle the plumbing code.
22

33
## Usage
44
You start by creating a client, and chain methods to configure your request and response:
@@ -50,7 +50,43 @@ You can even configure a range of features like credentials and cookies using th
5050
Not every feature is shown in these examples, but every method is fully code-documented for IntelliSense so it's easy to just use the client.
5151

5252
### Error handling
53-
By default HTTP errors will be raised as `ApiException` (or `AggregateException` if you explicitly call `.Result` or `.Wait()`), and you can add your own validation by overriding `IRequest.ValidateResponse`. For example, you could raise an exception if the API returns a non-HTTP error. (You can disable this by setting `IRequest.ThrowErrors = false`.)
53+
HTTP errors (such as HTTP Not Found) will be raised as `ApiException`, and you can add your own validation by overriding `Request.ValidateResponse`. For example, you could raise application errors from the API as client exceptions. (You can disable these exceptions by setting `IRequest.RaiseErrors = false`.)
54+
55+
When an HTTP request fails, you can find out why by checking the exception object. This contains the `HttpResponseMessage` (which includes the HTTP details like the request message, HTTP status code, headers, and response body) and `IResponse` (which provides a convenient way to read the response body).
56+
57+
For example:
58+
```c#
59+
/// <summary>Get a value from the API.</summary>
60+
/// <param name="key">The value key.</param>
61+
/// <exception cref="KeyNotFoundException">The key could not be found.</exception>
62+
/// <exception cref="CustomApiExeption">The remote application returned an error message.</exception>
63+
public async Task<string> GetValue(string key)
64+
{
65+
try
66+
{
67+
return await client
68+
.Get("api/dictionary")
69+
.WithArgument("key", key)
70+
.AsString();
71+
}
72+
catch(ApiException exception)
73+
{
74+
// key not found
75+
if(exception.ResponseMessage.StatusCode == HttpStatusCode.NotFound)
76+
throw new KeyNotFoundException("The key could not be found.")
77+
78+
// remote application error
79+
if(exception.ResponseMessage.Content != null)
80+
{
81+
exception.Response.RaiseErrors = false; // disable validation so we can read response content
82+
throw new CustomApiException(await exception.Response.AsString(), exception);
83+
}
84+
85+
// unhandled exception
86+
throw;
87+
}
88+
}
89+
```
5490

5591
### Synchronous use
5692
The client is designed to take advantage of the `async` and `await` keywords in .NET 4.5, but you can use the client synchronously:
@@ -68,6 +104,8 @@ Or if you don't need the response:
68104
client.PostAsync("ideas", new Idea()).Wait();
69105
```
70106

107+
Note: `Result` and `Await()` will wrap any exceptions thrown by the client into an `AggregateException`.
108+
71109
**Beware:** mixing blocking and asynchronous code within UI applications (like a web project) can lead to deadlocks. (If the only asynchronous code is the client itself, you should be fine doing this.) For further information, see _[Parallel Programming with .NET: Await, and UI, and deadlocks! Oh my!](http://blogs.msdn.com/b/pfxteam/archive/2011/01/13/10115163.aspx)_ (Stephen Toub, MSDN) and _[Don't Block on Async Code](http://nitoprograms.blogspot.ca/2012/07/dont-block-on-async-code.html)_ (Stephen Cleary).
72110

73111
## Installation

0 commit comments

Comments
 (0)