diff --git a/src/WebJobs.Script.Grpc/Channel/GrpcWorkerChannel.cs b/src/WebJobs.Script.Grpc/Channel/GrpcWorkerChannel.cs index 88fcda9b91..789322d3f6 100644 --- a/src/WebJobs.Script.Grpc/Channel/GrpcWorkerChannel.cs +++ b/src/WebJobs.Script.Grpc/Channel/GrpcWorkerChannel.cs @@ -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; diff --git a/src/WebJobs.Script.Grpc/Channel/GrpcWorkerChannelFactory.cs b/src/WebJobs.Script.Grpc/Channel/GrpcWorkerChannelFactory.cs index 5bdb4386e2..b808325b72 100644 --- a/src/WebJobs.Script.Grpc/Channel/GrpcWorkerChannelFactory.cs +++ b/src/WebJobs.Script.Grpc/Channel/GrpcWorkerChannelFactory.cs @@ -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; diff --git a/src/WebJobs.Script.Grpc/GrpcServiceCollectionsExtensions.cs b/src/WebJobs.Script.Grpc/GrpcServiceCollectionsExtensions.cs index 19a8578263..3e148d0513 100644 --- a/src/WebJobs.Script.Grpc/GrpcServiceCollectionsExtensions.cs +++ b/src/WebJobs.Script.Grpc/GrpcServiceCollectionsExtensions.cs @@ -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.HttpProxyService; using Microsoft.Azure.WebJobs.Script.Workers.Rpc; using Microsoft.Extensions.DependencyInjection; diff --git a/src/WebJobs.Script.Grpc/WebJobs.Script.Grpc.csproj b/src/WebJobs.Script.Grpc/WebJobs.Script.Grpc.csproj index d544f908e5..cb059eea7b 100644 --- a/src/WebJobs.Script.Grpc/WebJobs.Script.Grpc.csproj +++ b/src/WebJobs.Script.Grpc/WebJobs.Script.Grpc.csproj @@ -18,7 +18,6 @@ - diff --git a/src/WebJobs.Script.WebHost/Middleware/FunctionInvocationMiddleware.cs b/src/WebJobs.Script.WebHost/Middleware/FunctionInvocationMiddleware.cs index 2d4ff1b23f..ee18bb9504 100644 --- a/src/WebJobs.Script.WebHost/Middleware/FunctionInvocationMiddleware.cs +++ b/src/WebJobs.Script.WebHost/Middleware/FunctionInvocationMiddleware.cs @@ -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; @@ -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; } diff --git a/src/WebJobs.Script.Grpc/Exceptions/HttpForwardingException.cs b/src/WebJobs.Script/Exceptions/HttpForwardingException.cs similarity index 90% rename from src/WebJobs.Script.Grpc/Exceptions/HttpForwardingException.cs rename to src/WebJobs.Script/Exceptions/HttpForwardingException.cs index b60daba49d..d00a25c2a7 100644 --- a/src/WebJobs.Script.Grpc/Exceptions/HttpForwardingException.cs +++ b/src/WebJobs.Script/Exceptions/HttpForwardingException.cs @@ -3,7 +3,7 @@ using System; -namespace Microsoft.Azure.WebJobs.Script.Grpc.Exceptions +namespace Microsoft.Azure.WebJobs.Script.Exceptions { internal class HttpForwardingException : Exception { diff --git a/src/WebJobs.Script.Grpc/Server/DefaultHttpProxyService.cs b/src/WebJobs.Script/HttpProxyService/DefaultHttpProxyService.cs similarity index 97% rename from src/WebJobs.Script.Grpc/Server/DefaultHttpProxyService.cs rename to src/WebJobs.Script/HttpProxyService/DefaultHttpProxyService.cs index 1d164c2c04..c7f2b7b9e9 100644 --- a/src/WebJobs.Script.Grpc/Server/DefaultHttpProxyService.cs +++ b/src/WebJobs.Script/HttpProxyService/DefaultHttpProxyService.cs @@ -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 { diff --git a/src/WebJobs.Script.Grpc/Server/IHttpProxyService.cs b/src/WebJobs.Script/HttpProxyService/IHttpProxyService.cs similarity index 93% rename from src/WebJobs.Script.Grpc/Server/IHttpProxyService.cs rename to src/WebJobs.Script/HttpProxyService/IHttpProxyService.cs index 7db2eeaf12..6f17d3f8e9 100644 --- a/src/WebJobs.Script.Grpc/Server/IHttpProxyService.cs +++ b/src/WebJobs.Script/HttpProxyService/IHttpProxyService.cs @@ -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 { diff --git a/src/WebJobs.Script.Grpc/Server/RetryProxyHandler.cs b/src/WebJobs.Script/HttpProxyService/RetryProxyHandler.cs similarity index 97% rename from src/WebJobs.Script.Grpc/Server/RetryProxyHandler.cs rename to src/WebJobs.Script/HttpProxyService/RetryProxyHandler.cs index 8659a0403b..3bbed3ae4c 100644 --- a/src/WebJobs.Script.Grpc/Server/RetryProxyHandler.cs +++ b/src/WebJobs.Script/HttpProxyService/RetryProxyHandler.cs @@ -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 { diff --git a/src/WebJobs.Script/ScriptHostBuilderExtensions.cs b/src/WebJobs.Script/ScriptHostBuilderExtensions.cs index 278bf4a78f..372c5954d2 100644 --- a/src/WebJobs.Script/ScriptHostBuilderExtensions.cs +++ b/src/WebJobs.Script/ScriptHostBuilderExtensions.cs @@ -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; @@ -289,6 +290,11 @@ public static IHostBuilder AddScriptHostCore(this IHostBuilder builder, ScriptAp // Core WebJobs/Script Host services services.AddSingleton(); + // 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(); + // HTTP Worker services.AddSingleton(); services.AddSingleton(); diff --git a/src/WebJobs.Script/WebJobs.Script.csproj b/src/WebJobs.Script/WebJobs.Script.csproj index bcccab5b9c..2117fb7ad7 100644 --- a/src/WebJobs.Script/WebJobs.Script.csproj +++ b/src/WebJobs.Script/WebJobs.Script.csproj @@ -73,6 +73,7 @@ + diff --git a/src/WebJobs.Script/Workers/Http/Configuration/HttpWorkerOptions.cs b/src/WebJobs.Script/Workers/Http/Configuration/HttpWorkerOptions.cs index efd253febc..df3de0bd3a 100644 --- a/src/WebJobs.Script/Workers/Http/Configuration/HttpWorkerOptions.cs +++ b/src/WebJobs.Script/Workers/Http/Configuration/HttpWorkerOptions.cs @@ -15,8 +15,16 @@ public class HttpWorkerOptions public int Port { get; set; } + /// + /// Gets or sets a value indicating whether the host will rebuild the initial invocation HTTP Request and send the copy to the worker process. + /// public bool EnableForwardingHttpRequest { get; set; } + /// + /// Gets or sets a value indicating whether the host will proxy the invocation HTTP request to the worker process. + /// + public bool EnableProxyingHttpRequest { get; set; } + public TimeSpan InitializationTimeout { get; set; } = TimeSpan.FromSeconds(30); } } diff --git a/src/WebJobs.Script/Workers/Http/Configuration/HttpWorkerOptionsSetup.cs b/src/WebJobs.Script/Workers/Http/Configuration/HttpWorkerOptionsSetup.cs index c2817d291e..78c3b62ab5 100644 --- a/src/WebJobs.Script/Workers/Http/Configuration/HttpWorkerOptionsSetup.cs +++ b/src/WebJobs.Script/Workers/Http/Configuration/HttpWorkerOptionsSetup.cs @@ -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; } diff --git a/src/WebJobs.Script/Workers/Http/DefaultHttpWorkerService.cs b/src/WebJobs.Script/Workers/Http/DefaultHttpWorkerService.cs index 825a8c24c7..c83d54196a 100644 --- a/src/WebJobs.Script/Workers/Http/DefaultHttpWorkerService.cs +++ b/src/WebJobs.Script/Workers/Http/DefaultHttpWorkerService.cs @@ -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; @@ -24,17 +25,24 @@ public class DefaultHttpWorkerService : IHttpWorkerService private readonly HttpWorkerOptions _httpWorkerOptions; private readonly ILogger _logger; private readonly bool _enableRequestTracing; - - public DefaultHttpWorkerService(IOptions httpWorkerOptions, ILoggerFactory loggerFactory, IEnvironment environment, IOptions scriptHostOptions) - : this(CreateHttpClient(httpWorkerOptions), httpWorkerOptions, loggerFactory.CreateLogger(), environment, scriptHostOptions) + private readonly IHttpProxyService _httpProxyService; + private readonly ScriptInvocationResult _successfulInvocationResult; + private readonly Uri _destinationPrefix; + private readonly string _userAgentString; + + public DefaultHttpWorkerService(IOptions httpWorkerOptions, ILoggerFactory loggerFactory, IEnvironment environment, + IOptions scriptHostOptions, IHttpProxyService httpProxyService) + : this(CreateHttpClient(httpWorkerOptions), httpWorkerOptions, loggerFactory.CreateLogger(), environment, scriptHostOptions, httpProxyService) { } - internal DefaultHttpWorkerService(HttpClient httpClient, IOptions httpWorkerOptions, ILogger logger, IEnvironment environment, IOptions scriptHostOptions) + internal DefaultHttpWorkerService(HttpClient httpClient, IOptions httpWorkerOptions, ILogger logger, IEnvironment environment, + IOptions 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) { @@ -47,6 +55,14 @@ internal DefaultHttpWorkerService(HttpClient httpClient, IOptions() + }; + + _destinationPrefix = new UriBuilder(WorkerConstants.HttpScheme, WorkerConstants.HostName, _httpWorkerOptions.Port).Uri; + _userAgentString = $"{HttpWorkerConstants.UserAgentHeaderValue}/{ScriptHost.Version}"; } private static HttpClient CreateHttpClient(IOptions httpWorkerOptions) @@ -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) { @@ -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); + } + catch (Exception exc) + { + scriptInvocationContext.ResultSource.TrySetException(exc); + } + } + internal async Task ProcessHttpInAndOutInvocationRequest(ScriptInvocationContext scriptInvocationContext) { _logger.CustomHandlerForwardingHttpTriggerInvocation(scriptInvocationContext.FunctionMetadata.Name, scriptInvocationContext.ExecutionContext.InvocationId); @@ -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) diff --git a/test/WebJobs.Script.Tests.Integration/ApplicationInsights/ApplicationInsightsTestFixture.cs b/test/WebJobs.Script.Tests.Integration/ApplicationInsights/ApplicationInsightsTestFixture.cs index a927b6a82b..42dde6d50b 100644 --- a/test/WebJobs.Script.Tests.Integration/ApplicationInsights/ApplicationInsightsTestFixture.cs +++ b/test/WebJobs.Script.Tests.Integration/ApplicationInsights/ApplicationInsightsTestFixture.cs @@ -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; diff --git a/test/WebJobs.Script.Tests/HttpProxyService/DefaultHttpProxyServiceTests.cs b/test/WebJobs.Script.Tests/HttpProxyService/DefaultHttpProxyServiceTests.cs index 3356ed2017..6dfa7b1aba 100644 --- a/test/WebJobs.Script.Tests/HttpProxyService/DefaultHttpProxyServiceTests.cs +++ b/test/WebJobs.Script.Tests/HttpProxyService/DefaultHttpProxyServiceTests.cs @@ -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; diff --git a/test/WebJobs.Script.Tests/HttpWorker/DefaultHttpWorkerServiceTests.cs b/test/WebJobs.Script.Tests/HttpWorker/DefaultHttpWorkerServiceTests.cs index 66435a166c..0d06160f47 100644 --- a/test/WebJobs.Script.Tests/HttpWorker/DefaultHttpWorkerServiceTests.cs +++ b/test/WebJobs.Script.Tests/HttpWorker/DefaultHttpWorkerServiceTests.cs @@ -12,9 +12,13 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; +using Microsoft.Azure.WebJobs.Script.Description; +using Microsoft.Azure.WebJobs.Script.Exceptions; using Microsoft.Azure.WebJobs.Script.Extensions; +using Microsoft.Azure.WebJobs.Script.HttpProxyService; using Microsoft.Azure.WebJobs.Script.Workers; using Microsoft.Azure.WebJobs.Script.Workers.Http; +using Microsoft.Extensions.Azure; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Net.Http.Headers; @@ -39,6 +43,7 @@ public class DefaultHttpWorkerServiceTests private int _defaultPort = 8090; private TestLogger _testLogger = new TestLogger("ServiceLogger"); private TestLogger _functionLogger = new TestLogger(TestFunctionName); + private Mock _mockHttpProxyService; public DefaultHttpWorkerServiceTests() { @@ -54,6 +59,7 @@ public DefaultHttpWorkerServiceTests() { FunctionTimeout = TimeSpan.FromMinutes(15) }; + _mockHttpProxyService = new Mock(MockBehavior.Strict); } public static IEnumerable TestLogs @@ -77,7 +83,7 @@ public async Task ProcessDefaultInvocationRequest_Succeeds() .ReturnsAsync(HttpWorkerTestUtilities.GetValidHttpResponseMessage()); _httpClient = new HttpClient(handlerMock.Object); - _defaultHttpWorkerService = new DefaultHttpWorkerService(_httpClient, new OptionsWrapper(_httpWorkerOptions), _testLogger, _testEnvironment, new OptionsWrapper(_scriptJobHostOptions)); + _defaultHttpWorkerService = new DefaultHttpWorkerService(_httpClient, new OptionsWrapper(_httpWorkerOptions), _testLogger, _testEnvironment, new OptionsWrapper(_scriptJobHostOptions), _mockHttpProxyService.Object); Assert.Equal(_httpClient.Timeout, _scriptJobHostOptions.FunctionTimeout.Value.Add(TimeSpan.FromMinutes(1))); var testScriptInvocationContext = HttpWorkerTestUtilities.GetScriptInvocationContext(TestFunctionName, _testInvocationId, _functionLogger); await _defaultHttpWorkerService.ProcessDefaultInvocationRequest(testScriptInvocationContext); @@ -108,7 +114,7 @@ public async Task ProcessDefaultInvocationRequest_CustomHandler_EnableRequestFor .ReturnsAsync(HttpWorkerTestUtilities.GetValidHttpResponseMessageWithJsonRes()); _httpClient = new HttpClient(handlerMock.Object); - _defaultHttpWorkerService = new DefaultHttpWorkerService(_httpClient, new OptionsWrapper(customHandlerOptions), _testLogger, _testEnvironment, new OptionsWrapper(scriptJobHostOptionsNoTimeout)); + _defaultHttpWorkerService = new DefaultHttpWorkerService(_httpClient, new OptionsWrapper(customHandlerOptions), _testLogger, _testEnvironment, new OptionsWrapper(scriptJobHostOptionsNoTimeout), _mockHttpProxyService.Object); Assert.Equal(_httpClient.Timeout, TimeSpan.FromMilliseconds(int.MaxValue)); var testScriptInvocationContext = HttpWorkerTestUtilities.GetSimpleHttpTriggerScriptInvocationContext(TestFunctionName, _testInvocationId, _functionLogger); await _defaultHttpWorkerService.InvokeAsync(testScriptInvocationContext); @@ -140,7 +146,7 @@ public async Task ProcessDefaultInvocationRequest_DataType_Binary_Succeeds() .ReturnsAsync(HttpWorkerTestUtilities.GetValidHttpResponseMessage_DataType_Binary_Data()); _httpClient = new HttpClient(handlerMock.Object); - _defaultHttpWorkerService = new DefaultHttpWorkerService(_httpClient, new OptionsWrapper(_httpWorkerOptions), _testLogger, _testEnvironment, new OptionsWrapper(_scriptJobHostOptions)); + _defaultHttpWorkerService = new DefaultHttpWorkerService(_httpClient, new OptionsWrapper(_httpWorkerOptions), _testLogger, _testEnvironment, new OptionsWrapper(_scriptJobHostOptions), _mockHttpProxyService.Object); var testScriptInvocationContext = HttpWorkerTestUtilities.GetScriptInvocationContext(TestFunctionName, _testInvocationId, _functionLogger, WebJobs.Script.Description.DataType.Binary); await _defaultHttpWorkerService.ProcessDefaultInvocationRequest(testScriptInvocationContext); var invocationResult = await testScriptInvocationContext.ResultSource.Task; @@ -176,7 +182,7 @@ public async Task ProcessDefaultInvocationRequest_BinaryData_Succeeds() .ReturnsAsync(HttpWorkerTestUtilities.GetValidHttpResponseMessage_Binary_Data()); _httpClient = new HttpClient(handlerMock.Object); - _defaultHttpWorkerService = new DefaultHttpWorkerService(_httpClient, new OptionsWrapper(_httpWorkerOptions), _testLogger, _testEnvironment, new OptionsWrapper(_scriptJobHostOptions)); + _defaultHttpWorkerService = new DefaultHttpWorkerService(_httpClient, new OptionsWrapper(_httpWorkerOptions), _testLogger, _testEnvironment, new OptionsWrapper(_scriptJobHostOptions), _mockHttpProxyService.Object); var testScriptInvocationContext = HttpWorkerTestUtilities.GetScriptInvocationContext(TestFunctionName, _testInvocationId, _functionLogger); await _defaultHttpWorkerService.ProcessDefaultInvocationRequest(testScriptInvocationContext); var invocationResult = await testScriptInvocationContext.ResultSource.Task; @@ -214,7 +220,7 @@ public async Task ProcessPing_Succeeds() .ReturnsAsync(HttpWorkerTestUtilities.GetSimpleNotFoundHttpResponseMessage()); _httpClient = new HttpClient(handlerMock.Object); - _defaultHttpWorkerService = new DefaultHttpWorkerService(_httpClient, new OptionsWrapper(_httpWorkerOptions), _testLogger, _testEnvironment, new OptionsWrapper(_scriptJobHostOptions)); + _defaultHttpWorkerService = new DefaultHttpWorkerService(_httpClient, new OptionsWrapper(_httpWorkerOptions), _testLogger, _testEnvironment, new OptionsWrapper(_scriptJobHostOptions), _mockHttpProxyService.Object); await _defaultHttpWorkerService.PingAsync(); handlerMock.VerifyAll(); } @@ -231,7 +237,7 @@ public async Task ProcessSimpleHttpTriggerInvocationRequest_Succeeds() .ReturnsAsync(HttpWorkerTestUtilities.GetValidSimpleHttpResponseMessage()); _httpClient = new HttpClient(handlerMock.Object); - _defaultHttpWorkerService = new DefaultHttpWorkerService(_httpClient, new OptionsWrapper(_httpWorkerOptions), _testLogger, _testEnvironment, new OptionsWrapper(_scriptJobHostOptions)); + _defaultHttpWorkerService = new DefaultHttpWorkerService(_httpClient, new OptionsWrapper(_httpWorkerOptions), _testLogger, _testEnvironment, new OptionsWrapper(_scriptJobHostOptions), _mockHttpProxyService.Object); var testScriptInvocationContext = HttpWorkerTestUtilities.GetSimpleHttpTriggerScriptInvocationContext(TestFunctionName, _testInvocationId, _functionLogger); await _defaultHttpWorkerService.ProcessHttpInAndOutInvocationRequest(testScriptInvocationContext); var invocationResult = await testScriptInvocationContext.ResultSource.Task; @@ -269,7 +275,7 @@ public async Task ProcessSimpleHttpTriggerInvocationRequest_CustomHandler_Enable .ReturnsAsync(HttpWorkerTestUtilities.GetValidSimpleHttpResponseMessage()); _httpClient = new HttpClient(handlerMock.Object); - _defaultHttpWorkerService = new DefaultHttpWorkerService(_httpClient, new OptionsWrapper(customHandlerOptions), _testLogger, _testEnvironment, new OptionsWrapper(_scriptJobHostOptions)); + _defaultHttpWorkerService = new DefaultHttpWorkerService(_httpClient, new OptionsWrapper(customHandlerOptions), _testLogger, _testEnvironment, new OptionsWrapper(_scriptJobHostOptions), _mockHttpProxyService.Object); var testScriptInvocationContext = HttpWorkerTestUtilities.GetSimpleHttpTriggerScriptInvocationContext(TestFunctionName, _testInvocationId, _functionLogger); await _defaultHttpWorkerService.InvokeAsync(testScriptInvocationContext); var invocationResult = await testScriptInvocationContext.ResultSource.Task; @@ -300,7 +306,7 @@ public void TestBuildAndGetUri(string pathValue, string expectedUriString) { Port = 8080, }; - DefaultHttpWorkerService defaultHttpWorkerService = new DefaultHttpWorkerService(new HttpClient(), new OptionsWrapper(testOptions), _testLogger, _testEnvironment, new OptionsWrapper(_scriptJobHostOptions)); + DefaultHttpWorkerService defaultHttpWorkerService = new DefaultHttpWorkerService(new HttpClient(), new OptionsWrapper(testOptions), _testLogger, _testEnvironment, new OptionsWrapper(_scriptJobHostOptions), _mockHttpProxyService.Object); Assert.Equal(expectedUriString, defaultHttpWorkerService.BuildAndGetUri(pathValue)); } @@ -308,7 +314,7 @@ public void TestBuildAndGetUri(string pathValue, string expectedUriString) public void AddHeadersTest() { HttpWorkerOptions testOptions = new HttpWorkerOptions(); - DefaultHttpWorkerService defaultHttpWorkerService = new DefaultHttpWorkerService(new HttpClient(), new OptionsWrapper(testOptions), _testLogger, _testEnvironment, new OptionsWrapper(_scriptJobHostOptions)); + DefaultHttpWorkerService defaultHttpWorkerService = new DefaultHttpWorkerService(new HttpClient(), new OptionsWrapper(testOptions), _testLogger, _testEnvironment, new OptionsWrapper(_scriptJobHostOptions), _mockHttpProxyService.Object); HttpRequestMessage input = new HttpRequestMessage(); string invocationId = Guid.NewGuid().ToString(); @@ -332,7 +338,7 @@ public async Task ProcessSimpleHttpTriggerInvocationRequest_Sets_ExpectedResult( }); _httpClient = new HttpClient(handlerMock.Object); - _defaultHttpWorkerService = new DefaultHttpWorkerService(_httpClient, new OptionsWrapper(_httpWorkerOptions), _testLogger, _testEnvironment, new OptionsWrapper(_scriptJobHostOptions)); + _defaultHttpWorkerService = new DefaultHttpWorkerService(_httpClient, new OptionsWrapper(_httpWorkerOptions), _testLogger, _testEnvironment, new OptionsWrapper(_scriptJobHostOptions), _mockHttpProxyService.Object); var testScriptInvocationContext = HttpWorkerTestUtilities.GetSimpleHttpTriggerScriptInvocationContext(TestFunctionName, _testInvocationId, _functionLogger); await _defaultHttpWorkerService.ProcessHttpInAndOutInvocationRequest(testScriptInvocationContext); var invocationResult = await testScriptInvocationContext.ResultSource.Task; @@ -364,7 +370,7 @@ public async Task ProcessDefaultInvocationRequest_JsonResponse_Succeeds() .ReturnsAsync(HttpWorkerTestUtilities.GetHttpResponseMessageWithJsonContent()); _httpClient = new HttpClient(handlerMock.Object); - _defaultHttpWorkerService = new DefaultHttpWorkerService(_httpClient, new OptionsWrapper(_httpWorkerOptions), _testLogger, _testEnvironment, new OptionsWrapper(_scriptJobHostOptions)); + _defaultHttpWorkerService = new DefaultHttpWorkerService(_httpClient, new OptionsWrapper(_httpWorkerOptions), _testLogger, _testEnvironment, new OptionsWrapper(_scriptJobHostOptions), _mockHttpProxyService.Object); var testScriptInvocationContext = HttpWorkerTestUtilities.GetScriptInvocationContext(TestFunctionName, _testInvocationId, _functionLogger); await _defaultHttpWorkerService.ProcessDefaultInvocationRequest(testScriptInvocationContext); var invocationResult = await testScriptInvocationContext.ResultSource.Task; @@ -384,7 +390,7 @@ public async Task ProcessDefaultInvocationRequest_OkResponse_InvalidBody_Throws( .ReturnsAsync(HttpWorkerTestUtilities.GetValidHttpResponseMessage_JsonType_InvalidContent()); _httpClient = new HttpClient(handlerMock.Object); - _defaultHttpWorkerService = new DefaultHttpWorkerService(_httpClient, new OptionsWrapper(_httpWorkerOptions), _testLogger, _testEnvironment, new OptionsWrapper(_scriptJobHostOptions)); + _defaultHttpWorkerService = new DefaultHttpWorkerService(_httpClient, new OptionsWrapper(_httpWorkerOptions), _testLogger, _testEnvironment, new OptionsWrapper(_scriptJobHostOptions), _mockHttpProxyService.Object); var testScriptInvocationContext = HttpWorkerTestUtilities.GetScriptInvocationContext(TestFunctionName, _testInvocationId, _functionLogger); await _defaultHttpWorkerService.ProcessDefaultInvocationRequest(testScriptInvocationContext); InvalidOperationException recodedEx = await Assert.ThrowsAsync(async () => await testScriptInvocationContext.ResultSource.Task); @@ -404,7 +410,7 @@ public async Task ProcessDefaultInvocationRequest_InvalidMediaType_Throws() .ReturnsAsync(HttpWorkerTestUtilities.GetHttpResponseMessageWithStringContent()); _httpClient = new HttpClient(handlerMock.Object); - _defaultHttpWorkerService = new DefaultHttpWorkerService(_httpClient, new OptionsWrapper(_httpWorkerOptions), _testLogger, _testEnvironment, new OptionsWrapper(_scriptJobHostOptions)); + _defaultHttpWorkerService = new DefaultHttpWorkerService(_httpClient, new OptionsWrapper(_httpWorkerOptions), _testLogger, _testEnvironment, new OptionsWrapper(_scriptJobHostOptions), _mockHttpProxyService.Object); var testScriptInvocationContext = HttpWorkerTestUtilities.GetScriptInvocationContext(TestFunctionName, _testInvocationId, _testLogger); await _defaultHttpWorkerService.ProcessDefaultInvocationRequest(testScriptInvocationContext); InvalidOperationException recodedEx = await Assert.ThrowsAsync(async () => await testScriptInvocationContext.ResultSource.Task); @@ -426,7 +432,7 @@ public async Task ProcessDefaultInvocationRequest_BadRequestResponse_Throws() }); _httpClient = new HttpClient(handlerMock.Object); - _defaultHttpWorkerService = new DefaultHttpWorkerService(_httpClient, new OptionsWrapper(_httpWorkerOptions), _testLogger, _testEnvironment, new OptionsWrapper(_scriptJobHostOptions)); + _defaultHttpWorkerService = new DefaultHttpWorkerService(_httpClient, new OptionsWrapper(_httpWorkerOptions), _testLogger, _testEnvironment, new OptionsWrapper(_scriptJobHostOptions), _mockHttpProxyService.Object); var testScriptInvocationContext = HttpWorkerTestUtilities.GetScriptInvocationContext(TestFunctionName, _testInvocationId, _testLogger); await _defaultHttpWorkerService.ProcessDefaultInvocationRequest(testScriptInvocationContext); await Assert.ThrowsAsync(async () => await testScriptInvocationContext.ResultSource.Task); @@ -445,7 +451,7 @@ public void ProcessOutputLogs_Succeeds(HttpScriptInvocationResult httpScriptInvo .ReturnsAsync(HttpWorkerTestUtilities.GetValidHttpResponseMessage()); _httpClient = new HttpClient(handlerMock.Object); - _defaultHttpWorkerService = new DefaultHttpWorkerService(_httpClient, new OptionsWrapper(_httpWorkerOptions), _testLogger, _testEnvironment, new OptionsWrapper(_scriptJobHostOptions)); + _defaultHttpWorkerService = new DefaultHttpWorkerService(_httpClient, new OptionsWrapper(_httpWorkerOptions), _testLogger, _testEnvironment, new OptionsWrapper(_scriptJobHostOptions), _mockHttpProxyService.Object); _defaultHttpWorkerService.ProcessLogsFromHttpResponse(HttpWorkerTestUtilities.GetScriptInvocationContext(TestFunctionName, _testInvocationId, _functionLogger), httpScriptInvocationResult); var testLogs = _functionLogger.GetLogMessages(); if (httpScriptInvocationResult.Logs != null && httpScriptInvocationResult.Logs.Any()) @@ -472,7 +478,7 @@ public void TestPathValue(string functionName, CustomHandlerType type, bool enab Type = type, EnableForwardingHttpRequest = enableForwardingHttpRequest, }; - DefaultHttpWorkerService defaultHttpWorkerService = new DefaultHttpWorkerService(new HttpClient(), new OptionsWrapper(testOptions), _testLogger, _testEnvironment, new OptionsWrapper(_scriptJobHostOptions)); + DefaultHttpWorkerService defaultHttpWorkerService = new DefaultHttpWorkerService(new HttpClient(), new OptionsWrapper(testOptions), _testLogger, _testEnvironment, new OptionsWrapper(_scriptJobHostOptions), _mockHttpProxyService.Object); string actualValue = defaultHttpWorkerService.GetPathValue(testOptions, functionName, testHttpRequest); Assert.Equal(actualValue, expectedValue); } @@ -489,7 +495,7 @@ public async Task IsWorkerReady_Returns_False() .Throws(new HttpRequestException("Invalid http worker service", new SocketException())); _httpClient = new HttpClient(handlerMock.Object); - _defaultHttpWorkerService = new DefaultHttpWorkerService(_httpClient, new OptionsWrapper(_httpWorkerOptions), _testLogger, _testEnvironment, new OptionsWrapper(_scriptJobHostOptions)); + _defaultHttpWorkerService = new DefaultHttpWorkerService(_httpClient, new OptionsWrapper(_httpWorkerOptions), _testLogger, _testEnvironment, new OptionsWrapper(_scriptJobHostOptions), _mockHttpProxyService.Object); bool workerReady = await _defaultHttpWorkerService.IsWorkerReady(CancellationToken.None); Assert.False(workerReady); @@ -510,12 +516,128 @@ public async Task IsWorkerReady_Returns_True() .ReturnsAsync(HttpWorkerTestUtilities.GetValidHttpResponseMessage()); _httpClient = new HttpClient(handlerMock.Object); - _defaultHttpWorkerService = new DefaultHttpWorkerService(_httpClient, new OptionsWrapper(_httpWorkerOptions), _testLogger, _testEnvironment, new OptionsWrapper(_scriptJobHostOptions)); + _defaultHttpWorkerService = new DefaultHttpWorkerService(_httpClient, new OptionsWrapper(_httpWorkerOptions), _testLogger, _testEnvironment, new OptionsWrapper(_scriptJobHostOptions), _mockHttpProxyService.Object); bool workerReady = await _defaultHttpWorkerService.IsWorkerReady(CancellationToken.None); Assert.True(workerReady); } + [Fact] + public async Task ProxyInvocationRequest_Success() + { + var testUri = new Uri("http://localhost:7071/api/test"); + var testInvocationId = Guid.NewGuid(); + var testFunctionName = "TestFunction"; + + // When there is a simple HttpTrigger and EnableHttpProxyingRequest is set to true proxy, proxying service should be called and invocation result set to true upon completion + var testScriptInvocationContext = HttpWorkerTestUtilities.GetSimpleHttpTriggerScriptInvocationContext(testFunctionName, testInvocationId, _functionLogger); + var mockHttpClient = new Mock(); + HttpWorkerOptions testOptions = new HttpWorkerOptions + { + EnableProxyingHttpRequest = true + }; + + var mockHttpRequest = SetUpMockHttpRequestForProxying(); + + // Add the mocked HttpRequest to the ScriptInvocationContext + testScriptInvocationContext.Inputs = new List<(string Name, DataType Type, object Val)> + { + ("req", DataType.Undefined, mockHttpRequest.Object) + }; + + testScriptInvocationContext.Inputs = new List<(string Name, DataType Type, object Val)> { ("req", DataType.Undefined, mockHttpRequest.Object) }; + + _mockHttpProxyService + .Setup(m => m.StartForwarding(testScriptInvocationContext, It.IsAny())) + .Verifiable(); + + _mockHttpProxyService + .Setup(m => m.EnsureSuccessfulForwardingAsync(testScriptInvocationContext)) + .Returns(Task.CompletedTask) + .Verifiable(); + + _defaultHttpWorkerService = new DefaultHttpWorkerService( + mockHttpClient.Object, + new OptionsWrapper(testOptions), + _testLogger, + _testEnvironment, + new OptionsWrapper(_scriptJobHostOptions), + _mockHttpProxyService.Object); + + await _defaultHttpWorkerService.InvokeAsync(testScriptInvocationContext); + + _mockHttpProxyService.Verify(m => m.StartForwarding(testScriptInvocationContext, It.IsAny()), Times.Once); + _mockHttpProxyService.Verify(m => m.EnsureSuccessfulForwardingAsync(testScriptInvocationContext), Times.Once); + Assert.True(testScriptInvocationContext.ResultSource.Task.IsCompletedSuccessfully); + } + + [Fact] + public async Task ProxyInvocationRequest_ThrowsException_WhenHttpRequestIsMissing() + { + var mockHttpClient = new Mock(); + var testInvocationId = Guid.NewGuid(); + var testFunctionName = "TestFunction"; + var testScriptInvocationContext = HttpWorkerTestUtilities.GetSimpleHttpTriggerScriptInvocationContext(testFunctionName, testInvocationId, _functionLogger); + + _defaultHttpWorkerService = new DefaultHttpWorkerService( + mockHttpClient.Object, + new OptionsWrapper(_httpWorkerOptions), + _testLogger, + _testEnvironment, + new OptionsWrapper(_scriptJobHostOptions), + _mockHttpProxyService.Object); + + await _defaultHttpWorkerService.ProxyInvocationRequest(testScriptInvocationContext); + + Assert.True(testScriptInvocationContext.ResultSource.Task.IsFaulted); + Assert.IsType(testScriptInvocationContext.ResultSource.Task.Exception.InnerException); + Assert.Equal("Cannot proxy the HttpTrigger function TestFunction without an input of type HttpRequest.", testScriptInvocationContext.ResultSource.Task.Exception.InnerException.Message); + } + + [Fact] + public async Task ProxyInvocationRequest_HandlesForwardingError() + { + var mockHttpClient = new Mock(); + var testUri = new Uri("http://localhost:7071/api/test"); + var testInvocationId = Guid.NewGuid(); + var testFunctionName = "TestFunction"; + var testScriptInvocationContext = HttpWorkerTestUtilities.GetSimpleHttpTriggerScriptInvocationContext(testFunctionName, testInvocationId, _functionLogger); + + var mockHttpRequest = SetUpMockHttpRequestForProxying(); + + // Add the mocked HttpRequest to the ScriptInvocationContext + testScriptInvocationContext.Inputs = new List<(string Name, DataType Type, object Val)> + { + ("req", DataType.Undefined, mockHttpRequest.Object) + }; + + testScriptInvocationContext.Inputs = new List<(string Name, DataType Type, object Val)> { ("req", DataType.Undefined, mockHttpRequest.Object) }; + + _mockHttpProxyService + .Setup(m => m.StartForwarding(testScriptInvocationContext, It.IsAny())) + .Verifiable(); + + _mockHttpProxyService + .Setup(m => m.EnsureSuccessfulForwardingAsync(testScriptInvocationContext)) + .ThrowsAsync(new HttpForwardingException("Forwarding failed")) + .Verifiable(); + + _defaultHttpWorkerService = new DefaultHttpWorkerService( + mockHttpClient.Object, + new OptionsWrapper(_httpWorkerOptions), + _testLogger, + _testEnvironment, + new OptionsWrapper(_scriptJobHostOptions), + _mockHttpProxyService.Object); + + await _defaultHttpWorkerService.ProxyInvocationRequest(testScriptInvocationContext); + + _mockHttpProxyService.Verify(m => m.StartForwarding(testScriptInvocationContext, It.IsAny()), Times.Once); + _mockHttpProxyService.Verify(m => m.EnsureSuccessfulForwardingAsync(testScriptInvocationContext), Times.Once); + Assert.True(testScriptInvocationContext.ResultSource.Task.IsFaulted); + Assert.IsType(testScriptInvocationContext.ResultSource.Task.Exception.InnerException); + } + private async void ValidateDefaultInvocationRequest(HttpRequestMessage httpRequestMessage) { Assert.Contains($"{HttpWorkerConstants.UserAgentHeaderValue}/{ScriptHost.Version}", httpRequestMessage.Headers.UserAgent.ToString()); @@ -594,5 +716,17 @@ private void RequestHandler(HttpRequestMessage httpRequestMessage) { //used for tests that do not need request validation } + + private Mock SetUpMockHttpRequestForProxying() + { + var mockHttpRequest = new Mock(); + var mockHttpContext = new Mock(); + var headers = new HeaderDictionary(); + mockHttpRequest.SetupGet(r => r.Headers).Returns(headers); + mockHttpRequest.SetupGet(r => r.HttpContext).Returns(mockHttpContext.Object); + mockHttpContext.Setup(mockHttpContext => mockHttpContext.Items.ContainsKey(ScriptConstants.AzureFunctionsHttpTriggerContext)).Returns(true); + + return mockHttpRequest; + } } } diff --git a/test/WebJobs.Script.Tests/Workers/RetryProxyHandlerTests.cs b/test/WebJobs.Script.Tests/Workers/RetryProxyHandlerTests.cs index 3c42f2eae6..31f8a9c412 100644 --- a/test/WebJobs.Script.Tests/Workers/RetryProxyHandlerTests.cs +++ b/test/WebJobs.Script.Tests/Workers/RetryProxyHandlerTests.cs @@ -1,14 +1,11 @@ // 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 System.Collections.Generic; using System.Net.Http; using System.Reflection; using System.Threading; using System.Threading.Tasks; -using System.Web.Http; -using Microsoft.Azure.WebJobs.Script.Grpc; +using Microsoft.Azure.WebJobs.Script.HttpProxyService; using Microsoft.Extensions.Logging.Abstractions; using Xunit; diff --git a/test/WebJobs.Script.Tests/Workers/Rpc/GrpcWorkerChannelTests.cs b/test/WebJobs.Script.Tests/Workers/Rpc/GrpcWorkerChannelTests.cs index 6489f6c756..578feb65ea 100644 --- a/test/WebJobs.Script.Tests/Workers/Rpc/GrpcWorkerChannelTests.cs +++ b/test/WebJobs.Script.Tests/Workers/Rpc/GrpcWorkerChannelTests.cs @@ -17,6 +17,7 @@ using Microsoft.Azure.WebJobs.Script.Grpc; using Microsoft.Azure.WebJobs.Script.Grpc.Eventing; using Microsoft.Azure.WebJobs.Script.Grpc.Messages; +using Microsoft.Azure.WebJobs.Script.HttpProxyService; using Microsoft.Azure.WebJobs.Script.Workers; using Microsoft.Azure.WebJobs.Script.Workers.FunctionDataCache; using Microsoft.Azure.WebJobs.Script.Workers.Rpc;