diff --git a/release_notes.md b/release_notes.md index 6048dbc510..882bd8c6ec 100644 --- a/release_notes.md +++ b/release_notes.md @@ -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) diff --git a/src/WebJobs.Script.Grpc/Channel/GrpcWorkerChannel.cs b/src/WebJobs.Script.Grpc/Channel/GrpcWorkerChannel.cs index 88fcda9b91..7b897bfea0 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.Http; 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..1c0113967a 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.Http; 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..d79f651e44 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.Http; 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/Description/Workers/ScriptInvocationResult.cs b/src/WebJobs.Script/Description/Workers/ScriptInvocationResult.cs index 997e903fc2..3a47282118 100644 --- a/src/WebJobs.Script/Description/Workers/ScriptInvocationResult.cs +++ b/src/WebJobs.Script/Description/Workers/ScriptInvocationResult.cs @@ -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.Empty + }; + public object Return { get; set; } public IDictionary Outputs { get; set; } 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/Http/DefaultHttpProxyService.cs similarity index 97% rename from src/WebJobs.Script.Grpc/Server/DefaultHttpProxyService.cs rename to src/WebJobs.Script/Http/DefaultHttpProxyService.cs index 1d164c2c04..d7e6828024 100644 --- a/src/WebJobs.Script.Grpc/Server/DefaultHttpProxyService.cs +++ b/src/WebJobs.Script/Http/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.Http { internal class DefaultHttpProxyService : IHttpProxyService, IDisposable { diff --git a/src/WebJobs.Script/Http/DefaultHttpRouteManager.cs b/src/WebJobs.Script/Http/DefaultHttpRouteManager.cs index 7dc8a145b5..2f87b0af86 100644 --- a/src/WebJobs.Script/Http/DefaultHttpRouteManager.cs +++ b/src/WebJobs.Script/Http/DefaultHttpRouteManager.cs @@ -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 diff --git a/src/WebJobs.Script.Grpc/Server/IHttpProxyService.cs b/src/WebJobs.Script/Http/IHttpProxyService.cs similarity index 94% rename from src/WebJobs.Script.Grpc/Server/IHttpProxyService.cs rename to src/WebJobs.Script/Http/IHttpProxyService.cs index 7db2eeaf12..e5cbaae763 100644 --- a/src/WebJobs.Script.Grpc/Server/IHttpProxyService.cs +++ b/src/WebJobs.Script/Http/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.Http { public interface IHttpProxyService { diff --git a/src/WebJobs.Script/Http/IHttpRoutesManager.cs b/src/WebJobs.Script/Http/IHttpRoutesManager.cs index 3df8ae284c..a973d117fd 100644 --- a/src/WebJobs.Script/Http/IHttpRoutesManager.cs +++ b/src/WebJobs.Script/Http/IHttpRoutesManager.cs @@ -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 diff --git a/src/WebJobs.Script.Grpc/Server/RetryProxyHandler.cs b/src/WebJobs.Script/Http/RetryProxyHandler.cs similarity index 98% rename from src/WebJobs.Script.Grpc/Server/RetryProxyHandler.cs rename to src/WebJobs.Script/Http/RetryProxyHandler.cs index 8659a0403b..8c21e321f1 100644 --- a/src/WebJobs.Script.Grpc/Server/RetryProxyHandler.cs +++ b/src/WebJobs.Script/Http/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.Http { internal sealed class RetryProxyHandler : DelegatingHandler { 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..c8a7f629a0 100644 --- a/src/WebJobs.Script/Workers/Http/Configuration/HttpWorkerOptions.cs +++ b/src/WebJobs.Script/Workers/Http/Configuration/HttpWorkerOptions.cs @@ -15,8 +15,19 @@ public class HttpWorkerOptions public int Port { get; set; } + /// + /// Gets or sets a value indicating whether the host will forward the request to the worker process. + /// + /// + /// 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..aea4ee70f0 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.Http; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -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, ILoggerFactory loggerFactory, IEnvironment environment, IOptions scriptHostOptions) - : this(CreateHttpClient(httpWorkerOptions), httpWorkerOptions, loggerFactory.CreateLogger(), environment, scriptHostOptions) + 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 +54,9 @@ internal DefaultHttpWorkerService(HttpClient httpClient, IOptions httpWorkerOptions) @@ -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); @@ -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) diff --git a/test/WebJobs.Script.Tests.Integration/ApplicationInsights/ApplicationInsightsTestFixture.cs b/test/WebJobs.Script.Tests.Integration/ApplicationInsights/ApplicationInsightsTestFixture.cs index a927b6a82b..29becaa359 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.Http; 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..81313edb38 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.Http; 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..4e28afac1d 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.Http; 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..1905c27423 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.Http; 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..767b9e232d 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.Http; using Microsoft.Azure.WebJobs.Script.Workers; using Microsoft.Azure.WebJobs.Script.Workers.FunctionDataCache; using Microsoft.Azure.WebJobs.Script.Workers.Rpc;