Skip to content

Custom Handler Proxying #11035

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 27 commits into from
Jun 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions release_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@
- Improvements to coldstart pipeline (#11102).
- Update Python Worker Version to [4.38.0](https://github.com/Azure/azure-functions-python-worker/releases/tag/4.38.0)
- Only start the Diagnostic Events flush logs timer when events are present, preventing unnecessary flush attempts (#11100).
- Enable HTTP proxying for custom handlers (#11035)
- Switched memory usage reporting to use CGroup metrics by default for Linux consumption (#11114)
1 change: 1 addition & 0 deletions src/WebJobs.Script.Grpc/Channel/GrpcWorkerChannel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
using Microsoft.Azure.WebJobs.Script.Grpc.Eventing;
using Microsoft.Azure.WebJobs.Script.Grpc.Extensions;
using Microsoft.Azure.WebJobs.Script.Grpc.Messages;
using Microsoft.Azure.WebJobs.Script.Http;
using Microsoft.Azure.WebJobs.Script.ManagedDependencies;
using Microsoft.Azure.WebJobs.Script.Workers;
using Microsoft.Azure.WebJobs.Script.Workers.Rpc;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Microsoft.Azure.WebJobs.Script.Diagnostics;
using Microsoft.Azure.WebJobs.Script.Eventing;
using Microsoft.Azure.WebJobs.Script.Grpc.Eventing;
using Microsoft.Azure.WebJobs.Script.Http;
using Microsoft.Azure.WebJobs.Script.Workers;
using Microsoft.Azure.WebJobs.Script.Workers.Rpc;
using Microsoft.Azure.WebJobs.Script.Workers.SharedMemoryDataTransfer;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT License. See License.txt in the project root for license information.

using Microsoft.Azure.WebJobs.Script.Grpc.Messages;
using Microsoft.Azure.WebJobs.Script.Http;
using Microsoft.Azure.WebJobs.Script.Workers.Rpc;
using Microsoft.Extensions.DependencyInjection;

Expand Down
1 change: 0 additions & 1 deletion src/WebJobs.Script.Grpc/WebJobs.Script.Grpc.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
<PackageReference Include="System.IO.FileSystem.Primitives" Version="4.3.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556" PrivateAssets="all" />
<PackageReference Include="System.Threading.Channels" Version="8.0.0" />
<PackageReference Include="Yarp.ReverseProxy" Version="2.0.1" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,8 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization.Policy;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
Expand Down Expand Up @@ -57,9 +55,9 @@ public async Task Invoke(HttpContext context)
int nestedProxiesCount = GetNestedProxiesCount(context, functionExecution);
IActionResult result = await GetResultAsync(context, functionExecution);

if (context.Items.TryGetValue(ScriptConstants.HttpProxyingEnabled, out var value))
if (context.Items.TryGetValue(ScriptConstants.HttpProxyingEnabled, out var httpProxyingEnabled))
{
if (value?.ToString() == bool.TrueString)
if (httpProxyingEnabled?.ToString() == bool.TrueString)
{
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,17 @@
// Licensed under the MIT License. See License.txt in the project root for license information.

using System.Collections.Generic;
using System.Collections.Immutable;

namespace Microsoft.Azure.WebJobs.Script.Description
{
public class ScriptInvocationResult
{
public static readonly ScriptInvocationResult Success = new ScriptInvocationResult
{
Outputs = ImmutableDictionary<string, object>.Empty
};

public object Return { get; set; }

public IDictionary<string, object> Outputs { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

using System;

namespace Microsoft.Azure.WebJobs.Script.Grpc.Exceptions
namespace Microsoft.Azure.WebJobs.Script.Exceptions
{
internal class HttpForwardingException : Exception
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Azure.WebJobs.Script.Description;
using Microsoft.Azure.WebJobs.Script.Grpc.Exceptions;
using Microsoft.Azure.WebJobs.Script.Exceptions;
using Microsoft.Azure.WebJobs.Script.Workers;
using Microsoft.Extensions.Logging;
using Yarp.ReverseProxy.Forwarder;

namespace Microsoft.Azure.WebJobs.Script.Grpc
namespace Microsoft.Azure.WebJobs.Script.Http
{
internal class DefaultHttpProxyService : IHttpProxyService, IDisposable
{
Expand Down
3 changes: 0 additions & 3 deletions src/WebJobs.Script/Http/DefaultHttpRouteManager.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

using System;
using Microsoft.Azure.WebJobs.Script.WebHost;

namespace Microsoft.Azure.WebJobs.Script.Http
{
internal class DefaultHttpRouteManager : IHttpRoutesManager
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs.Script.Description;

namespace Microsoft.Azure.WebJobs.Script.Grpc
namespace Microsoft.Azure.WebJobs.Script.Http
{
public interface IHttpProxyService
{
Expand Down
2 changes: 0 additions & 2 deletions src/WebJobs.Script/Http/IHttpRoutesManager.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

using System;

namespace Microsoft.Azure.WebJobs.Script
{
public interface IHttpRoutesManager
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;

namespace Microsoft.Azure.WebJobs.Script.Grpc
namespace Microsoft.Azure.WebJobs.Script.Http
{
internal sealed class RetryProxyHandler : DelegatingHandler
{
Expand Down
1 change: 1 addition & 0 deletions src/WebJobs.Script/WebJobs.Script.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
<PackageReference Include="System.Text.Json" Version="8.0.5" />
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" />
<PackageReference Include="System.Threading.Channels" Version="8.0.0" />
<PackageReference Include="Yarp.ReverseProxy" Version="2.0.1" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,19 @@ public class HttpWorkerOptions

public int Port { get; set; }

/// <summary>
/// Gets or sets a value indicating whether the host will forward the request to the worker process.
/// </summary>
/// <remarks>
/// The host will rebuild the initial invocation HTTP Request and send the copy to the worker process.
/// </remarks>
public bool EnableForwardingHttpRequest { get; set; }

/// <summary>
/// Gets or sets a value indicating whether the host will proxy the invocation HTTP request to the worker process.
/// </summary>
public bool EnableProxyingHttpRequest { get; set; }

public TimeSpan InitializationTimeout { get; set; } = TimeSpan.FromSeconds(30);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public void Configure(HttpWorkerOptions options)
ConfigureWorkerDescription(options, customHandlerSection);
if (options.Type == CustomHandlerType.None)
{
// CustomHandlerType.None is only for maitaining backward compatibilty with httpWorker section.
// CustomHandlerType.None is only for maintaining backward compatability with httpWorker section.
_logger.LogWarning($"CustomHandlerType {CustomHandlerType.None} is not supported. Defaulting to {CustomHandlerType.Http}.");
options.Type = CustomHandlerType.Http;
}
Expand Down
55 changes: 50 additions & 5 deletions src/WebJobs.Script/Workers/Http/DefaultHttpWorkerService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
using Microsoft.Azure.WebJobs.Script.Description;
using Microsoft.Azure.WebJobs.Script.Diagnostics.Extensions;
using Microsoft.Azure.WebJobs.Script.Extensions;
using Microsoft.Azure.WebJobs.Script.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

Expand All @@ -24,17 +25,23 @@ public class DefaultHttpWorkerService : IHttpWorkerService
private readonly HttpWorkerOptions _httpWorkerOptions;
private readonly ILogger _logger;
private readonly bool _enableRequestTracing;
private readonly IHttpProxyService _httpProxyService;
private readonly Uri _destinationPrefix;
private readonly string _userAgentString;

public DefaultHttpWorkerService(IOptions<HttpWorkerOptions> httpWorkerOptions, ILoggerFactory loggerFactory, IEnvironment environment, IOptions<ScriptJobHostOptions> scriptHostOptions)
: this(CreateHttpClient(httpWorkerOptions), httpWorkerOptions, loggerFactory.CreateLogger<DefaultHttpWorkerService>(), environment, scriptHostOptions)
public DefaultHttpWorkerService(IOptions<HttpWorkerOptions> httpWorkerOptions, ILoggerFactory loggerFactory, IEnvironment environment,
IOptions<ScriptJobHostOptions> scriptHostOptions, IHttpProxyService httpProxyService)
: this(CreateHttpClient(httpWorkerOptions), httpWorkerOptions, loggerFactory.CreateLogger<DefaultHttpWorkerService>(), environment, scriptHostOptions, httpProxyService)
{
}

internal DefaultHttpWorkerService(HttpClient httpClient, IOptions<HttpWorkerOptions> httpWorkerOptions, ILogger logger, IEnvironment environment, IOptions<ScriptJobHostOptions> scriptHostOptions)
internal DefaultHttpWorkerService(HttpClient httpClient, IOptions<HttpWorkerOptions> httpWorkerOptions, ILogger logger, IEnvironment environment,
IOptions<ScriptJobHostOptions> scriptHostOptions, IHttpProxyService httpProxyService)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
_httpWorkerOptions = httpWorkerOptions.Value ?? throw new ArgumentNullException(nameof(httpWorkerOptions.Value));
_httpProxyService = httpProxyService ?? throw new ArgumentNullException(nameof(httpProxyService));
_enableRequestTracing = environment.IsCoreTools();
if (scriptHostOptions.Value.FunctionTimeout == null)
{
Expand All @@ -47,6 +54,9 @@ internal DefaultHttpWorkerService(HttpClient httpClient, IOptions<HttpWorkerOpti
// Set 1 minute greater than FunctionTimeout to ensure invoction failure due to timeout is raised before httpClient raises operation cancelled exception
_httpClient.Timeout = scriptHostOptions.Value.FunctionTimeout.Value.Add(TimeSpan.FromMinutes(1));
}

_destinationPrefix = new UriBuilder(WorkerConstants.HttpScheme, WorkerConstants.HostName, _httpWorkerOptions.Port).Uri;
_userAgentString = $"{HttpWorkerConstants.UserAgentHeaderValue}/{ScriptHost.Version}";
}

private static HttpClient CreateHttpClient(IOptions<HttpWorkerOptions> httpWorkerOptions)
Expand All @@ -61,16 +71,44 @@ public Task InvokeAsync(ScriptInvocationContext scriptInvocationContext)
{
if (scriptInvocationContext.FunctionMetadata.IsHttpInAndOutFunction())
{
if (_httpWorkerOptions.EnableProxyingHttpRequest)
{
return ProxyInvocationRequest(scriptInvocationContext);
}

// type is empty for httpWorker section. EnableForwardingHttpRequest is opt-in for custom handler section.
if (_httpWorkerOptions.Type == CustomHandlerType.None || _httpWorkerOptions.EnableForwardingHttpRequest)
{
return ProcessHttpInAndOutInvocationRequest(scriptInvocationContext);
}
return ProcessDefaultInvocationRequest(scriptInvocationContext);
}

return ProcessDefaultInvocationRequest(scriptInvocationContext);
}

internal async Task ProxyInvocationRequest(ScriptInvocationContext scriptInvocationContext)
{
try
{
if (!scriptInvocationContext.TryGetHttpRequest(out HttpRequest httpRequest))
{
throw new InvalidOperationException($"Cannot proxy the HttpTrigger function {scriptInvocationContext.FunctionMetadata.Name} without an input of type {nameof(HttpRequest)}.");
}

AddProxyingHeaders(httpRequest, scriptInvocationContext.ExecutionContext.InvocationId.ToString());

// YARP only requires the destination prefix. The path and query string are added by the YARP proxy during SendAsync using info from the HttpContext.
_httpProxyService.StartForwarding(scriptInvocationContext, _destinationPrefix);

await _httpProxyService.EnsureSuccessfulForwardingAsync(scriptInvocationContext); // this will throw if forwarding is unsuccessful
scriptInvocationContext.ResultSource.SetResult(ScriptInvocationResult.Success);
}
catch (Exception exc)
{
scriptInvocationContext.ResultSource.TrySetException(exc);
}
}

internal async Task ProcessHttpInAndOutInvocationRequest(ScriptInvocationContext scriptInvocationContext)
{
_logger.CustomHandlerForwardingHttpTriggerInvocation(scriptInvocationContext.FunctionMetadata.Name, scriptInvocationContext.ExecutionContext.InvocationId);
Expand Down Expand Up @@ -162,7 +200,14 @@ internal void AddHeaders(HttpRequestMessage httpRequest, string invocationId)
{
httpRequest.Headers.Add(HttpWorkerConstants.HostVersionHeaderName, ScriptHost.Version);
httpRequest.Headers.Add(HttpWorkerConstants.InvocationIdHeaderName, invocationId);
httpRequest.Headers.UserAgent.ParseAdd($"{HttpWorkerConstants.UserAgentHeaderValue}/{ScriptHost.Version}");
httpRequest.Headers.UserAgent.ParseAdd(_userAgentString);
}

private void AddProxyingHeaders(HttpRequest httpRequest, string invocationId)
{
// if there are existing headers, override them
httpRequest.Headers[HttpWorkerConstants.HostVersionHeaderName] = ScriptHost.Version;
httpRequest.Headers[HttpWorkerConstants.InvocationIdHeaderName] = invocationId;
}

internal string GetPathValue(HttpWorkerOptions httpWorkerOptions, string functionName, HttpRequest httpRequest)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using Microsoft.Azure.WebJobs.Script.Diagnostics;
using Microsoft.Azure.WebJobs.Script.Eventing;
using Microsoft.Azure.WebJobs.Script.Grpc;
using Microsoft.Azure.WebJobs.Script.Http;
using Microsoft.Azure.WebJobs.Script.Workers;
using Microsoft.Azure.WebJobs.Script.Workers.Rpc;
using Microsoft.Azure.WebJobs.Script.Workers.SharedMemoryDataTransfer;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
using System.Linq;
using Microsoft.AspNetCore.Http;
using Microsoft.Azure.WebJobs.Script.Description;
using Microsoft.Azure.WebJobs.Script.Grpc;
using Microsoft.Azure.WebJobs.Script.Http;
using Microsoft.Extensions.Logging;
using Moq;
using Xunit;
Expand Down
Loading