Skip to content

Commit 52169fc

Browse files
authored
Handling HttpRequestException in CallHttpAsync() (#1718)
These changes add a way to handle when this.httpClient.SendAsync() throws an HttpRequestException and propagates the exception to customer code. Before this change, the extension would throw an exception about serialization instead of HttpRequestException. By handling the HttpRequestException in TaskHttpActivityShim, an HttpRequestException is thrown with a descriptive message. Resolves #1669
1 parent 7617df9 commit 52169fc

File tree

4 files changed

+89
-19
lines changed

4 files changed

+89
-19
lines changed

release_notes.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,7 @@
55
## Bug Fixes:
66
- Properly used update management API URLs after a successful slot swap (#1716)
77
- Fix race condition when multiple apps start with local RPC endpoints on the same VM in parallel. (#1719)
8+
- Fix CallHttpAsync() to throw an HttpRequestException instead of a serialization exception if the target endpoint doesn't exist (#1718)
89

10+
## Breaking changes
11+
- Fix CallHttpAsync() to throw an HttpRequestException instead of a serialization exception if the target endpoint doesn't exist (#1718). This is a breaking change if you were handling `HttpRequestException`s by catching `FunctionFailedException`s.

src/WebJobs.Extensions.DurableTask/ContextImplementations/DurableOrchestrationContext.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -690,8 +690,14 @@ internal async Task<TResult> CallDurableTaskFunctionAsync<TResult>(
690690
{
691691
// Check to see if CallHttpAsync() threw a TimeoutException
692692
// In this case, we want to throw a TimeoutException instead of a FunctionFailedException
693-
if (functionName.Equals(HttpOptions.HttpTaskActivityReservedName) && e.InnerException is TimeoutException)
693+
if (functionName.Equals(HttpOptions.HttpTaskActivityReservedName) &&
694+
(e.InnerException is TimeoutException || e.InnerException is HttpRequestException))
694695
{
696+
if (e.InnerException is HttpRequestException)
697+
{
698+
throw new HttpRequestException(e.Message);
699+
}
700+
695701
throw e.InnerException;
696702
}
697703

src/WebJobs.Extensions.DurableTask/Listener/TaskHttpActivityShim.cs

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -42,32 +42,40 @@ public async override Task<string> RunAsync(TaskContext context, string rawInput
4242
HttpRequestMessage requestMessage = await this.ConvertToHttpRequestMessage(durableHttpRequest);
4343

4444
HttpResponseMessage response;
45-
if (durableHttpRequest.Timeout == null)
45+
try
4646
{
47-
response = await this.httpClient.SendAsync(requestMessage);
48-
}
49-
else
50-
{
51-
try
47+
if (durableHttpRequest.Timeout == null)
5248
{
53-
using (CancellationTokenSource cts = new CancellationTokenSource())
54-
{
55-
cts.CancelAfter(durableHttpRequest.Timeout.Value);
56-
response = await this.httpClient.SendAsync(requestMessage, cts.Token);
57-
}
49+
response = await this.httpClient.SendAsync(requestMessage);
5850
}
59-
catch (OperationCanceledException ex)
51+
else
6052
{
61-
TimeoutException e = new TimeoutException(ex.Message + $" Reached user specified timeout: {durableHttpRequest.Timeout.Value}.");
53+
try
54+
{
55+
using (CancellationTokenSource cts = new CancellationTokenSource())
56+
{
57+
cts.CancelAfter(durableHttpRequest.Timeout.Value);
58+
response = await this.httpClient.SendAsync(requestMessage, cts.Token);
59+
}
60+
}
61+
catch (OperationCanceledException ex)
62+
{
63+
TimeoutException e = new TimeoutException(ex.Message + $" Reached user specified timeout: {durableHttpRequest.Timeout.Value}.");
6264

63-
string details = Utils.SerializeCause(e, this.config.ErrorDataConverter);
64-
throw new TaskFailureException(e.Message, e, details);
65+
string details = Utils.SerializeCause(e, this.config.ErrorDataConverter);
66+
throw new TaskFailureException(e.Message, e, details);
67+
}
6568
}
66-
}
6769

68-
DurableHttpResponse durableHttpResponse = await DurableHttpResponse.CreateDurableHttpResponseWithHttpResponseMessage(response);
70+
DurableHttpResponse durableHttpResponse = await DurableHttpResponse.CreateDurableHttpResponseWithHttpResponseMessage(response);
6971

70-
return JsonConvert.SerializeObject(durableHttpResponse);
72+
return JsonConvert.SerializeObject(durableHttpResponse);
73+
}
74+
catch (HttpRequestException ex)
75+
{
76+
string details = Utils.SerializeCause(ex, this.config.ErrorDataConverter);
77+
throw new TaskFailureException(ex.Message, ex, details);
78+
}
7179
}
7280

7381
private static DurableHttpRequest ReconstructDurableHttpRequest(string serializedRequest)

test/Common/DurableHttpTests.cs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,44 @@ public async Task DurableHttpAsync_Synchronous_TimeoutException(string storagePr
362362
}
363363
}
364364

365+
/// <summary>
366+
/// End-to-end test which checks if the CallHttpAsync Orchestrator fails when the
367+
/// target url doesn't exist and throws an HttpRequestException.
368+
/// </summary>
369+
[Theory]
370+
[Trait("Category", PlatformSpecificHelpers.TestCategory)]
371+
[MemberData(nameof(TestDataGenerator.GetFullFeaturedStorageProviderOptions), MemberType = typeof(TestDataGenerator))]
372+
public async Task DurableHttpAsync_Synchronous_HttpRequestException(string storageProvider)
373+
{
374+
HttpMessageHandler httpMessageHandler = MockSynchronousHttpMessageHandlerWithHttpRequestException();
375+
376+
using (ITestHost host = TestHelpers.GetJobHost(
377+
this.loggerProvider,
378+
nameof(this.DurableHttpAsync_Synchronous_HttpRequestException),
379+
enableExtendedSessions: false,
380+
storageProviderType: storageProvider,
381+
durableHttpMessageHandler: new DurableHttpMessageHandlerFactory(httpMessageHandler)))
382+
{
383+
await host.StartAsync();
384+
385+
Dictionary<string, string> headers = new Dictionary<string, string>();
386+
headers.Add("Accept", "application/json");
387+
TestDurableHttpRequest testRequest = new TestDurableHttpRequest(
388+
httpMethod: HttpMethod.Get,
389+
headers: headers);
390+
391+
string functionName = nameof(TestOrchestrations.CallHttpAsyncOrchestrator);
392+
var client = await host.StartOrchestratorAsync(functionName, testRequest, this.output);
393+
var status = await client.WaitForCompletionAsync(this.output);
394+
395+
var output = status?.Output;
396+
Assert.Contains("No such host is known.", output.ToString());
397+
Assert.Equal(OrchestrationRuntimeStatus.Failed, status?.RuntimeStatus);
398+
399+
await host.StopAsync();
400+
}
401+
}
402+
365403
/// <summary>
366404
/// End-to-end test which checks if the UserAgent header is set in the HttpResponseMessage.
367405
/// </summary>
@@ -1509,6 +1547,21 @@ private static HttpMessageHandler MockSynchronousHttpMessageHandlerWithTimeoutEx
15091547
return handlerMock.Object;
15101548
}
15111549

1550+
private static HttpMessageHandler MockSynchronousHttpMessageHandlerWithHttpRequestException()
1551+
{
1552+
HttpResponseMessage httpResponseMessage = CreateTestHttpResponseMessage(HttpStatusCode.NotFound);
1553+
1554+
httpResponseMessage.Content = new ExceptionThrowingContent(new HttpRequestException("No such host is known."));
1555+
1556+
var handlerMock = new Mock<HttpMessageHandler>(MockBehavior.Strict);
1557+
handlerMock
1558+
.Protected()
1559+
.Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
1560+
.ReturnsAsync(httpResponseMessage);
1561+
1562+
return handlerMock.Object;
1563+
}
1564+
15121565
private static HttpMessageHandler MockHttpMessageHandlerCheckUserAgent()
15131566
{
15141567
HttpResponseMessage okHttpResponseMessage = CreateTestHttpResponseMessage(HttpStatusCode.OK);

0 commit comments

Comments
 (0)