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

Open
wants to merge 15 commits into
base: dev
Choose a base branch
from
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.HttpProxyService;
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.HttpProxyService;
using Microsoft.Azure.WebJobs.Script.Workers;
using Microsoft.Azure.WebJobs.Script.Workers.Rpc;
using Microsoft.Azure.WebJobs.Script.Workers.SharedMemoryDataTransfer;
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 @@ -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.HttpProxyService
{
internal class DefaultHttpProxyService : IHttpProxyService, IDisposable
{
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.HttpProxyService
{
public interface IHttpProxyService
{
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.HttpProxyService
{
internal sealed class RetryProxyHandler : DelegatingHandler
{
Expand Down
6 changes: 6 additions & 0 deletions src/WebJobs.Script/ScriptHostBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
using Microsoft.Azure.WebJobs.Script.ExtensionBundle;
using Microsoft.Azure.WebJobs.Script.FileProvisioning;
using Microsoft.Azure.WebJobs.Script.Http;
using Microsoft.Azure.WebJobs.Script.HttpProxyService;
using Microsoft.Azure.WebJobs.Script.ManagedDependencies;
using Microsoft.Azure.WebJobs.Script.Scale;
using Microsoft.Azure.WebJobs.Script.Workers;
Expand Down Expand Up @@ -289,6 +290,11 @@ public static IHostBuilder AddScriptHostCore(this IHostBuilder builder, ScriptAp
// Core WebJobs/Script Host services
services.AddSingleton<ScriptHost>();

// Add http proxying services - this is used with http streaming workers and custom handlers when enabled
// http streaming capabilities are known following worker initialization so that info isn't available at this stage
services.AddHttpForwarder();
services.AddSingleton<IHttpProxyService, DefaultHttpProxyService>();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this be conditionally registered? Only when custom handlers is enabled?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the cleanup and refactor, this service will also be used for HTTP Proxying for worker http streaming. Information about streaming support/capabilities won't be known until there is a worker init response (which occurs much later than this stage).


// HTTP Worker
services.AddSingleton<IHttpWorkerProcessFactory, HttpWorkerProcessFactory>();
services.AddSingleton<IHttpWorkerChannelFactory, HttpWorkerChannelFactory>();
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,16 @@ public class HttpWorkerOptions

public int Port { get; set; }

/// <summary>
/// Gets or sets a value indicating whether the host will rebuild the initial invocation HTTP Request and send the copy to the worker process.
/// </summary>
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
62 changes: 57 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.HttpProxyService;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

Expand All @@ -24,17 +25,24 @@ public class DefaultHttpWorkerService : IHttpWorkerService
private readonly HttpWorkerOptions _httpWorkerOptions;
private readonly ILogger _logger;
private readonly bool _enableRequestTracing;

public DefaultHttpWorkerService(IOptions<HttpWorkerOptions> httpWorkerOptions, ILoggerFactory loggerFactory, IEnvironment environment, IOptions<ScriptJobHostOptions> scriptHostOptions)
: this(CreateHttpClient(httpWorkerOptions), httpWorkerOptions, loggerFactory.CreateLogger<DefaultHttpWorkerService>(), environment, scriptHostOptions)
private readonly IHttpProxyService _httpProxyService;
private readonly ScriptInvocationResult _successfulInvocationResult;
private readonly Uri _destinationPrefix;
private readonly string _userAgentString;

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 +55,14 @@ 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));
}

_successfulInvocationResult = new ScriptInvocationResult()
{
Outputs = new Dictionary<string, object>()
};

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this ever get set outside of the ctor? Could this be a const? It doesnt look like its made up of anything that would require it to be init in here no?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't get set outside of the ctor, but ScriptHost.Version isn't a constant so this can't be init as a constant outside of the ctor at the top of the file.

}

private static HttpClient CreateHttpClient(IOptions<HttpWorkerOptions> httpWorkerOptions)
Expand All @@ -61,6 +77,11 @@ 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)
{
Expand All @@ -71,6 +92,29 @@ public Task InvokeAsync(ScriptInvocationContext 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);
scriptInvocationContext.ResultSource.SetResult(_successfulInvocationResult);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the downstream code updates this _successfulInvocationResult instance? I see we use a shared instance for all invocations.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, the downstream code does not. This value is set to allow the function invocation to continue, but its value is never used (similar to how http proxying flows). We ignore the function invocation result at this step so that the actual response just flows through.

}
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 +206,15 @@ 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;
httpRequest.Headers.UserAgent = _userAgentString;
}

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.HttpProxyService;
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.HttpProxyService;
using Microsoft.Extensions.Logging;
using Moq;
using Xunit;
Expand Down
Loading
Loading