Skip to content

Commit f003627

Browse files
authored
Update http proxy to handle client disconnect scenario (#10688)
1 parent 6166c81 commit f003627

File tree

8 files changed

+105
-1
lines changed

8 files changed

+105
-1
lines changed

release_notes.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
<!-- Please add your release notes in the following format:
44
- My change description (#PR)
55
-->
6+
67
- Allow for an output binding value of an invocation result to be null (#10698)
78
- Updated dotnet-isolated worker to 1.0.12.
89
- [Corrected the path for the prelaunch app location.](https://github.com/Azure/azure-functions-dotnet-worker/pull/2897)
910
- [Added net9 prelaunch app.](https://github.com/Azure/azure-functions-dotnet-worker/pull/2898)
11+
- Update the `DefaultHttpProxyService` to better handle client disconnect scenarios (#10688)
12+
- Replaced `InvalidOperationException` with `HttpForwardingException` when there is a ForwarderError
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
using System;
5+
6+
namespace Microsoft.Azure.WebJobs.Script.Grpc.Exceptions
7+
{
8+
internal class HttpForwardingException : Exception
9+
{
10+
public HttpForwardingException()
11+
{
12+
}
13+
14+
public HttpForwardingException(string message) : base(message)
15+
{
16+
}
17+
18+
public HttpForwardingException(string message, Exception innerException) : base(message, innerException)
19+
{
20+
}
21+
}
22+
}

src/WebJobs.Script.Grpc/Server/DefaultHttpProxyService.cs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using System.Threading.Tasks;
99
using Microsoft.AspNetCore.Http;
1010
using Microsoft.Azure.WebJobs.Script.Description;
11+
using Microsoft.Azure.WebJobs.Script.Grpc.Exceptions;
1112
using Microsoft.Azure.WebJobs.Script.Workers;
1213
using Microsoft.Extensions.Logging;
1314
using Yarp.ReverseProxy.Forwarder;
@@ -20,10 +21,12 @@ internal class DefaultHttpProxyService : IHttpProxyService, IDisposable
2021
private readonly IHttpForwarder _httpForwarder;
2122
private readonly HttpMessageInvoker _messageInvoker;
2223
private readonly ForwarderRequestConfig _forwarderRequestConfig;
24+
private readonly ILogger<DefaultHttpProxyService> _logger;
2325

2426
public DefaultHttpProxyService(IHttpForwarder httpForwarder, ILogger<DefaultHttpProxyService> logger)
2527
{
2628
_httpForwarder = httpForwarder ?? throw new ArgumentNullException(nameof(httpForwarder));
29+
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
2730

2831
_handler = new SocketsHttpHandler
2932
{
@@ -56,13 +59,23 @@ public async Task EnsureSuccessfulForwardingAsync(ScriptInvocationContext contex
5659

5760
if (httpProxyTaskResult is not ForwarderError.None)
5861
{
62+
_logger.LogDebug($"The function invocation failed to proxy the request with ForwarderError: {httpProxyTaskResult}");
63+
5964
Exception forwarderException = null;
6065
if (context.TryGetHttpRequest(out HttpRequest request))
6166
{
6267
forwarderException = request.HttpContext.GetForwarderErrorFeature()?.Exception;
6368
}
6469

65-
throw new InvalidOperationException($"Failed to proxy request with ForwarderError: {httpProxyTaskResult}", forwarderException);
70+
if (request.HttpContext.RequestAborted.IsCancellationRequested)
71+
{
72+
// Client disconnected, nothing we can do here but inform
73+
_logger.LogInformation("The client disconnected while the function was processing the request.");
74+
}
75+
else
76+
{
77+
throw new HttpForwardingException($"Failed to proxy request with ForwarderError: {httpProxyTaskResult}", forwarderException);
78+
}
6679
}
6780
}
6881

src/WebJobs.Script.Grpc/Server/RetryProxyHandler.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage
3737
{
3838
return await base.SendAsync(request, cancellationToken);
3939
}
40+
catch (TaskCanceledException) when (cancellationToken.IsCancellationRequested)
41+
{
42+
_logger.LogDebug("Request was canceled. Stopping retries.");
43+
throw new OperationCanceledException(cancellationToken);
44+
}
4045
catch (HttpRequestException) when (attemptCount < MaxRetries)
4146
{
4247
_logger.LogWarning("Failed to proxy request to the worker. Retrying in {delay}ms. Attempt {attemptCount} of {maxRetries}.",

src/WebJobs.Script.WebHost/Diagnostics/Extensions/ScriptHostServiceLoggerExtension.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,12 @@ public static class ScriptHostServiceLoggerExtension
188188
new EventId(530, nameof(LogHostInitializationSettings)),
189189
"{hostInitializationSettings}");
190190

191+
private static readonly Action<ILogger, string, Exception> _requestAborted =
192+
LoggerMessage.Define<string>(
193+
LogLevel.Debug,
194+
new EventId(531, nameof(RequestAborted)),
195+
"The request was aborted by the client (requestId: '{mS_ActivityId}').");
196+
191197
public static void HostStateChanged(this ILogger logger, ScriptHostState previousHostState, ScriptHostState newHostState)
192198
{
193199
var newState = newHostState.ToString();
@@ -205,6 +211,11 @@ public static void ExecutedHttpRequest(this ILogger logger, string mS_ActivityId
205211
_executedHttpRequest(logger, mS_ActivityId, identities, statusCode, duration, null);
206212
}
207213

214+
public static void RequestAborted(this ILogger logger, string mS_ActivityId)
215+
{
216+
_requestAborted(logger, mS_ActivityId, null);
217+
}
218+
208219
public static void ScriptHostServiceInitCanceledByRuntime(this ILogger logger)
209220
{
210221
_scriptHostServiceInitCanceledByRuntime(logger, null);
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.IO;
6+
using System.Threading.Tasks;
7+
using Microsoft.AspNetCore.Http;
8+
using Microsoft.Azure.WebJobs.Script.WebHost.Diagnostics.Extensions;
9+
using Microsoft.Extensions.Logging;
10+
11+
namespace Microsoft.Azure.WebJobs.Script.WebHost.Middleware
12+
{
13+
internal class HandleCancellationMiddleware
14+
{
15+
private readonly ILogger _logger;
16+
private readonly RequestDelegate _next;
17+
18+
public HandleCancellationMiddleware(RequestDelegate next, ILogger<HandleCancellationMiddleware> logger)
19+
{
20+
_logger = logger;
21+
_next = next;
22+
}
23+
24+
public async Task Invoke(HttpContext context)
25+
{
26+
var requestId = context.Request.HttpContext.Items[ScriptConstants.AzureFunctionsRequestIdKey].ToString();
27+
try
28+
{
29+
await _next.Invoke(context);
30+
31+
if (context.RequestAborted.IsCancellationRequested && !context.Response.HasStarted)
32+
{
33+
_logger.RequestAborted(requestId);
34+
context.Response.StatusCode = StatusCodes.Status499ClientClosedRequest;
35+
}
36+
}
37+
catch (Exception ex) when ((ex is OperationCanceledException || ex is IOException) && context.RequestAborted.IsCancellationRequested)
38+
{
39+
_logger.RequestAborted(requestId);
40+
41+
if (!context.Response.HasStarted)
42+
{
43+
context.Response.StatusCode = StatusCodes.Status499ClientClosedRequest;
44+
}
45+
}
46+
}
47+
}
48+
}

src/WebJobs.Script.WebHost/Middleware/SystemTraceMiddleware.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System;
55
using System.Diagnostics;
6+
using System.IO;
67
using System.Linq;
78
using System.Text;
89
using System.Threading.Tasks;

src/WebJobs.Script.WebHost/WebJobsApplicationBuilderExtension.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ public static IApplicationBuilder UseWebJobsScriptHost(this IApplicationBuilder
3737
builder.UseMiddleware<ClrOptimizationMiddleware>();
3838
builder.UseMiddleware<HttpRequestBodySizeMiddleware>();
3939
builder.UseMiddleware<SystemTraceMiddleware>();
40+
builder.UseMiddleware<HandleCancellationMiddleware>();
4041
builder.UseMiddleware<HostnameFixupMiddleware>();
4142
if (environment.IsAnyLinuxConsumption() || environment.IsAnyKubernetesEnvironment())
4243
{

0 commit comments

Comments
 (0)