Skip to content

Commit 63f8339

Browse files
Merge pull request #434 from ottomatic/issue_358
Issue #358: SendGridClient.SendEmailAsync now throws original exception
2 parents cdfda07 + cf50681 commit 63f8339

File tree

2 files changed

+206
-42
lines changed

2 files changed

+206
-42
lines changed

src/SendGrid/SendGridClient.cs

Lines changed: 70 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,6 @@ public class SendGridClient
5454
/// <returns>Interface to the SendGrid REST API</returns>
5555
public SendGridClient(IWebProxy webProxy, string apiKey, string host = null, Dictionary<string, string> requestHeaders = null, string version = "v3", string urlPath = null)
5656
{
57-
UrlPath = urlPath;
58-
Version = version;
59-
60-
var baseAddress = host ?? "https://api.sendgrid.com";
61-
var clientVersion = GetType().GetTypeInfo().Assembly.GetName().Version.ToString();
6257

6358
// Create client with WebProxy if set
6459
if (webProxy != null)
@@ -76,6 +71,58 @@ public SendGridClient(IWebProxy webProxy, string apiKey, string host = null, Dic
7671
client = new HttpClient();
7772
}
7873

74+
InitiateClient(apiKey, host, requestHeaders, version, urlPath);
75+
}
76+
77+
/// <summary>
78+
/// Constructor. Initializes a new instance of the <see cref="SendGridClient"/> class.
79+
/// </summary>
80+
/// <param name="httpClient">An optional http client which may me injected in order to facilitate testing.</param>
81+
/// <param name="apiKey">Your SendGrid API key.</param>
82+
/// <param name="host">Base url (e.g. https://api.sendgrid.com)</param>
83+
/// <param name="requestHeaders">A dictionary of request headers</param>
84+
/// <param name="version">API version, override AddVersion to customize</param>
85+
/// <param name="urlPath">Path to endpoint (e.g. /path/to/endpoint)</param>
86+
/// <returns>Interface to the SendGrid REST API</returns>
87+
public SendGridClient(HttpClient httpClient, string apiKey, string host = null, Dictionary<string, string> requestHeaders = null, string version = "v3", string urlPath = null)
88+
{
89+
client = (httpClient == null) ? new HttpClient() : httpClient;
90+
91+
InitiateClient(apiKey, host, requestHeaders, version, urlPath);
92+
}
93+
94+
95+
/// <summary>
96+
/// Initializes a new instance of the <see cref="SendGridClient"/> class.
97+
/// </summary>
98+
/// <param name="apiKey">Your SendGrid API key.</param>
99+
/// <param name="host">Base url (e.g. https://api.sendgrid.com)</param>
100+
/// <param name="requestHeaders">A dictionary of request headers</param>
101+
/// <param name="version">API version, override AddVersion to customize</param>
102+
/// <param name="urlPath">Path to endpoint (e.g. /path/to/endpoint)</param>
103+
/// <returns>Interface to the SendGrid REST API</returns>
104+
public SendGridClient(string apiKey, string host = null, Dictionary<string, string> requestHeaders = null, string version = "v3", string urlPath = null)
105+
: this(httpClient: null, apiKey: apiKey,host: host, requestHeaders: requestHeaders, version: version, urlPath: urlPath)
106+
{
107+
}
108+
109+
/// <summary>
110+
/// Common method to initiate internal fields regardless of which constructor was used.
111+
/// </summary>
112+
/// <param name="apiKey"></param>
113+
/// <param name="host"></param>
114+
/// <param name="requestHeaders"></param>
115+
/// <param name="version"></param>
116+
/// <param name="urlPath"></param>
117+
private void InitiateClient(string apiKey, string host, Dictionary<string, string> requestHeaders, string version, string urlPath)
118+
{
119+
UrlPath = urlPath;
120+
Version = version;
121+
122+
var baseAddress = host ?? "https://api.sendgrid.com";
123+
var clientVersion = GetType().GetTypeInfo().Assembly.GetName().Version.ToString();
124+
125+
79126
// standard headers
80127
client.BaseAddress = new Uri(baseAddress);
81128
Dictionary<string, string> headers = new Dictionary<string, string>
@@ -113,20 +160,7 @@ public SendGridClient(IWebProxy webProxy, string apiKey, string host = null, Dic
113160
client.DefaultRequestHeaders.Add(header.Key, header.Value);
114161
}
115162
}
116-
}
117163

118-
/// <summary>
119-
/// Initializes a new instance of the <see cref="SendGridClient"/> class.
120-
/// </summary>
121-
/// <param name="apiKey">Your SendGrid API key.</param>
122-
/// <param name="host">Base url (e.g. https://api.sendgrid.com)</param>
123-
/// <param name="requestHeaders">A dictionary of request headers</param>
124-
/// <param name="version">API version, override AddVersion to customize</param>
125-
/// <param name="urlPath">Path to endpoint (e.g. /path/to/endpoint)</param>
126-
/// <returns>Interface to the SendGrid REST API</returns>
127-
public SendGridClient(string apiKey, string host = null, Dictionary<string, string> requestHeaders = null, string version = "v3", string urlPath = null)
128-
: this(null, apiKey, host, requestHeaders, version, urlPath)
129-
{
130164
}
131165

132166
/// <summary>
@@ -192,40 +226,34 @@ public virtual AuthenticationHeaderValue AddAuthorization(KeyValuePair<string, s
192226
/// <param name="urlPath">The path to the API endpoint.</param>
193227
/// <param name="cancellationToken">Cancel the asynchronous call.</param>
194228
/// <returns>Response object</returns>
229+
/// <exception cref="Exception">The method will NOT catch and swallow exceptions generated by sending a request
230+
/// through the internal http client. Any underlying exception will pass right through.
231+
/// In particular, this means that you may expect
232+
/// a TimeoutException if you are not connected to the internet.</exception>
195233
public async Task<Response> RequestAsync(
196234
SendGridClient.Method method,
197235
string requestBody = null,
198236
string queryParams = null,
199237
string urlPath = null,
200238
CancellationToken cancellationToken = default(CancellationToken))
201239
{
202-
try
203-
{
204-
var endpoint = client.BaseAddress + BuildUrl(urlPath, queryParams);
205-
206-
// Build the request body
207-
StringContent content = null;
208-
if (requestBody != null)
209-
{
210-
content = new StringContent(requestBody, Encoding.UTF8, this.MediaType);
211-
}
240+
var endpoint = client.BaseAddress + BuildUrl(urlPath, queryParams);
212241

213-
// Build the final request
214-
var request = new HttpRequestMessage
215-
{
216-
Method = new HttpMethod(method.ToString()),
217-
RequestUri = new Uri(endpoint),
218-
Content = content
219-
};
220-
return await MakeRequest(request, cancellationToken).ConfigureAwait(false);
221-
}
222-
catch (Exception ex)
242+
// Build the request body
243+
StringContent content = null;
244+
if (requestBody != null)
223245
{
224-
var response = new HttpResponseMessage();
225-
var message = (ex is HttpRequestException) ? ".NET HttpRequestException" : ".NET Exception";
226-
response.Content = new StringContent(message + ", raw message: \n\n" + ex.Message);
227-
return new Response(response.StatusCode, response.Content, response.Headers);
246+
content = new StringContent(requestBody, Encoding.UTF8, this.MediaType);
228247
}
248+
249+
// Build the final request
250+
var request = new HttpRequestMessage
251+
{
252+
Method = new HttpMethod(method.ToString()),
253+
RequestUri = new Uri(endpoint),
254+
Content = content
255+
};
256+
return await MakeRequest(request, cancellationToken).ConfigureAwait(false);
229257
}
230258

231259
/// <summary>

tests/SendGrid.Tests/Integration.cs

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,11 @@
66
using System.Collections.Generic;
77
using System.Diagnostics;
88
using System.Net;
9+
using System.Net.Http;
910
using System.Threading.Tasks;
1011
using Xunit;
12+
using System.Threading;
13+
using System.Text;
1114

1215
public class IntegrationFixture : IDisposable
1316
{
@@ -5769,5 +5772,138 @@ public async Task TestWhitelabelLinksLinkIdSubuserPost()
57695772
var response = await sg.RequestAsync(method: SendGridClient.Method.POST, urlPath: "whitelabel/links/" + link_id + "/subuser", requestBody: data);
57705773
Assert.True(HttpStatusCode.OK == response.StatusCode);
57715774
}
5775+
5776+
[Theory]
5777+
[InlineData(200, "OK")]
5778+
[InlineData(301, "Moved permanently")]
5779+
[InlineData(401, "Unauthorized")]
5780+
[InlineData(503, "Service unavailable")]
5781+
public async Task TestTakesHttpClientFactoryAsConstructorArgumentAndUsesItInHttpCalls(HttpStatusCode httpStatusCode, string message)
5782+
{
5783+
var httpResponse = String.Format("<xml><result>{0}</result></xml>", message);
5784+
var httpMessageHandler = new FixedStatusAndMessageHttpMessageHandler(httpStatusCode, httpResponse);
5785+
HttpClient clientToInject = new HttpClient(httpMessageHandler);
5786+
5787+
var sg = new SendGridClient(clientToInject, fixture.apiKey);
5788+
5789+
var data = @"{
5790+
'username': 'jane@example.com'
5791+
}";
5792+
var json = JsonConvert.DeserializeObject<Object>(data);
5793+
data = json.ToString();
5794+
var link_id = "test_url_param";
5795+
var response = await sg.RequestAsync(method: SendGridClient.Method.POST, urlPath: "whitelabel/links/" + link_id + "/subuser", requestBody: data);
5796+
Assert.Equal(httpStatusCode, response.StatusCode);
5797+
Assert.Equal(httpResponse, response.Body.ReadAsStringAsync().Result);
5798+
}
5799+
5800+
5801+
/// <summary>
5802+
/// Tests the conditions in issue #358.
5803+
/// When an Http call times out while sending a message,
5804+
/// the client should NOT return a response code 200, but instead re-throw the exception.
5805+
/// </summary>
5806+
/// <returns></returns>
5807+
[Fact]
5808+
public async Task TestWhenHttpCallTimesOutThenExceptionIsThrown()
5809+
{
5810+
/* ****************************************************************************************
5811+
* Create a simple message.
5812+
* **************************************************************************************** */
5813+
var msg = new SendGridMessage();
5814+
msg.SetFrom(new EmailAddress("test@example.com"));
5815+
msg.AddTo(new EmailAddress("test@example.com"));
5816+
msg.SetSubject("Hello World from the SendGrid CSharp Library");
5817+
msg.AddContent(MimeType.Html, "HTML content");
5818+
5819+
/* ****************************************************************************************
5820+
* Here is where we ensure that the call to the SendEmailAsync() should lead to
5821+
* a TimeoutException being thrown by the HttpClient inside the SendGridClient object
5822+
* **************************************************************************************** */
5823+
var httpMessageHandler = new TimeOutExceptionThrowingHttpMessageHandler(20, "The operation timed out");
5824+
HttpClient clientToInject = new HttpClient(httpMessageHandler);
5825+
var sg = new SendGridClient(clientToInject, fixture.apiKey);
5826+
5827+
/* ****************************************************************************************
5828+
* Make the method call, expecting a an exception to be thrown.
5829+
* I don't care if the component code simply passes on the original exception or if it catches
5830+
* the original exception and throws another, custom exception. So I'll only
5831+
* assert that ANY exception is thrown.
5832+
* **************************************************************************************** */
5833+
var exceptionTask = Record.ExceptionAsync(async () =>
5834+
{
5835+
var response = await sg.SendEmailAsync(msg);
5836+
});
5837+
5838+
Assert.NotNull(exceptionTask);
5839+
5840+
var thrownException = exceptionTask.Result;
5841+
Assert.NotNull(thrownException);
5842+
5843+
// If we are certain that we don't want custom exceptions to be thrown,
5844+
// we can also test that the original exception was thrown
5845+
Assert.IsType(typeof(TimeoutException), thrownException);
5846+
5847+
5848+
}
5849+
5850+
}
5851+
5852+
public class FakeHttpMessageHandler : HttpMessageHandler
5853+
{
5854+
public virtual HttpResponseMessage Send(HttpRequestMessage request)
5855+
{
5856+
throw new NotImplementedException("Ensure you setup this method as part of your test.");
5857+
}
5858+
5859+
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
5860+
{
5861+
return Task.FromResult(Send(request));
5862+
}
5863+
}
5864+
5865+
public class FixedStatusAndMessageHttpMessageHandler : FakeHttpMessageHandler
5866+
{
5867+
private HttpStatusCode httpStatusCode;
5868+
private string message;
5869+
5870+
public FixedStatusAndMessageHttpMessageHandler(HttpStatusCode httpStatusCode, string message)
5871+
{
5872+
this.httpStatusCode = httpStatusCode;
5873+
this.message = message;
5874+
}
5875+
5876+
public override HttpResponseMessage Send(HttpRequestMessage request)
5877+
{
5878+
return new HttpResponseMessage(httpStatusCode)
5879+
{
5880+
Content = new StringContent(message)
5881+
};
5882+
}
5883+
5884+
}
5885+
5886+
/// <summary>
5887+
/// This message handler could be mocked using e.g. Moq.Mock, but the author of the test is
5888+
/// careful about introducing new dependecies in the test project, so creates a concrete
5889+
/// class instead.
5890+
/// </summary>
5891+
public class TimeOutExceptionThrowingHttpMessageHandler : FakeHttpMessageHandler
5892+
{
5893+
private int timeOutMilliseconds;
5894+
private string exceptionMessage;
5895+
5896+
public TimeOutExceptionThrowingHttpMessageHandler(int timeOutMilliseconds, string exceptionMessage)
5897+
{
5898+
this.timeOutMilliseconds = timeOutMilliseconds;
5899+
this.exceptionMessage = exceptionMessage;
5900+
}
5901+
5902+
public override HttpResponseMessage Send(HttpRequestMessage request)
5903+
{
5904+
Thread.Sleep(timeOutMilliseconds);
5905+
throw new TimeoutException(exceptionMessage);
5906+
}
5907+
57725908
}
57735909
}

0 commit comments

Comments
 (0)