Skip to content

Commit b1ab899

Browse files
committed
Latest runtime updates break catch all proxy routes when pointing to http trigger functions in the same function app.
1 parent e3e2b2a commit b1ab899

File tree

8 files changed

+182
-13
lines changed

8 files changed

+182
-13
lines changed

src/WebJobs.Script.WebHost/ProxyFunctionExecutor.cs

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,19 +22,90 @@ public class ProxyFunctionExecutor : IFuncExecutor
2222
{
2323
private readonly WebScriptHostManager _scriptHostManager;
2424
private readonly ISecretManager _secretManager;
25+
private readonly string _httpPrefix;
26+
2527
private WebHookReceiverManager _webHookReceiverManager;
2628

2729
internal ProxyFunctionExecutor(WebScriptHostManager scriptHostManager, WebHookReceiverManager webHookReceiverManager, ISecretManager secretManager)
2830
{
2931
_scriptHostManager = scriptHostManager;
3032
_webHookReceiverManager = webHookReceiverManager;
3133
_secretManager = secretManager;
34+
35+
_httpPrefix = HttpExtensionConstants.DefaultRoutePrefix;
36+
37+
if (_scriptHostManager.Instance != null)
38+
{
39+
var json = _scriptHostManager.Instance.ScriptConfig.HostConfig.HostConfigMetadata;
40+
41+
if (json != null && json["http"] != null && json["http"]["routePrefix"] != null)
42+
{
43+
_httpPrefix = json["http"]["routePrefix"].ToString().Trim(new char[] { '/' });
44+
}
45+
}
3246
}
3347

3448
public async Task ExecuteFuncAsync(string funcName, Dictionary<string, object> arguments, CancellationToken cancellationToken)
3549
{
3650
HttpRequestMessage request = arguments[ScriptConstants.AzureFunctionsHttpRequestKey] as HttpRequestMessage;
37-
var function = _scriptHostManager.GetHttpFunctionOrNull(request);
51+
52+
FunctionDescriptor function = null;
53+
var path = request.RequestUri.AbsolutePath.Trim(new char[] { '/' });
54+
55+
if (path.StartsWith(_httpPrefix))
56+
{
57+
path = path.Remove(0, _httpPrefix.Length);
58+
}
59+
60+
path = path.Trim(new char[] { '/' });
61+
62+
// This is a call to the local function app, before handing the route to asp.net to pick the FunctionDescriptor the following will be run:
63+
// 1. If the request maps to a local http trigger function name then that function will be picked.
64+
// 2. Else if the request maps to a custom route of a local http trigger function then that function will be picked
65+
// 3. Otherwise the request will be given to asp.net to pick the appropriate route.
66+
foreach (var func in _scriptHostManager.HttpFunctions.Values)
67+
{
68+
if (!func.Metadata.IsProxy)
69+
{
70+
if (path.Equals(func.Metadata.Name, StringComparison.OrdinalIgnoreCase))
71+
{
72+
function = func;
73+
break;
74+
}
75+
else
76+
{
77+
foreach (var binding in func.InputBindings)
78+
{
79+
if (binding.Metadata.IsTrigger)
80+
{
81+
string functionRoute = null;
82+
var jsonContent = binding.Metadata.Raw;
83+
if (jsonContent != null && jsonContent["route"] != null)
84+
{
85+
functionRoute = jsonContent["route"].ToString();
86+
}
87+
88+
// BUG: Known issue, This does not work on dynamic routes like products/{category:alpha}/{id:int?}
89+
if (!string.IsNullOrEmpty(functionRoute) && path.Equals(functionRoute, StringComparison.OrdinalIgnoreCase))
90+
{
91+
function = func;
92+
break;
93+
}
94+
}
95+
}
96+
97+
if (function != null)
98+
{
99+
break;
100+
}
101+
}
102+
}
103+
}
104+
105+
if (function == null)
106+
{
107+
function = _scriptHostManager.GetHttpFunctionOrNull(request);
108+
}
38109

39110
var functionRequestInvoker = new FunctionRequestInvoker(function, _secretManager);
40111
var response = await functionRequestInvoker.PreprocessRequestAsync(request);

src/WebJobs.Script/Host/ScriptHost.cs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ public class ScriptHost : JobHost
5555
internal static readonly TimeSpan DefaultFunctionTimeout = TimeSpan.FromMinutes(5);
5656
internal static readonly TimeSpan MaxFunctionTimeout = TimeSpan.FromMinutes(10);
5757
private static readonly Regex FunctionNameValidationRegex = new Regex(@"^[a-z][a-z0-9_\-]{0,127}$(?<!^host$)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
58+
private static readonly Regex ProxyNameValidationRegex = new Regex(@"[^a-zA-Z0-9_-]", RegexOptions.Compiled | RegexOptions.IgnoreCase);
5859
public static readonly string Version = GetAssemblyFileVersion(typeof(ScriptHost).Assembly);
5960
private ScriptSettingsManager _settingsManager;
6061
private bool _shutdownScheduled;
@@ -1016,8 +1017,8 @@ private Collection<FunctionMetadata> LoadProxyRoutes(string proxiesJson)
10161017
{
10171018
try
10181019
{
1019-
// Proxy names should follow the same naming restrictions as in function names.
1020-
ValidateName(route.Name, true);
1020+
// Proxy names should follow the same naming restrictions as in function names. If not, invalid characters will be removed.
1021+
var proxyName = NormalizeProxyName(route.Name);
10211022

10221023
var proxyMetadata = new FunctionMetadata();
10231024

@@ -1035,7 +1036,7 @@ private Collection<FunctionMetadata> LoadProxyRoutes(string proxiesJson)
10351036

10361037
proxyMetadata.Bindings.Add(bindingMetadata);
10371038

1038-
proxyMetadata.Name = route.Name;
1039+
proxyMetadata.Name = proxyName;
10391040
proxyMetadata.ScriptType = ScriptType.Unknown;
10401041
proxyMetadata.IsProxy = true;
10411042

@@ -1121,6 +1122,11 @@ internal static void ValidateName(string name, bool isProxy = false)
11211122
}
11221123
}
11231124

1125+
internal static string NormalizeProxyName(string name)
1126+
{
1127+
return ProxyNameValidationRegex.Replace(name, string.Empty);
1128+
}
1129+
11241130
/// <summary>
11251131
/// Determines which script should be considered the "primary" entry point script.
11261132
/// </summary>
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"bindings": [
3+
{
4+
"authLevel": "function",
5+
"name": "req",
6+
"type": "httpTrigger",
7+
"direction": "in",
8+
"route": "myroute"
9+
},
10+
{
11+
"name": "$return",
12+
"type": "http",
13+
"direction": "out"
14+
}
15+
],
16+
"disabled": false
17+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
public static string Run(HttpRequestMessage req) => "Pong";

test/WebJobs.Script.Tests.E2E/Functions/wwwroot/proxies.json

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,36 @@
4545
"responseOverrides": {
4646
"response.statusCode": "200"
4747
}
48+
},
49+
"catchAll": {
50+
"matchCondition": {
51+
"route": "api/{*path}"
52+
},
53+
"backendUri": "http://localhost/api/Ping",
54+
"requestOverrides": {
55+
"backend.request.headers.accept": "text/plain"
56+
}
57+
},
58+
"catchAllRoutes": {
59+
"matchCondition": {
60+
"route": "/{*path}"
61+
},
62+
"backendUri": "http://localhost/myroute",
63+
"requestOverrides": {
64+
"backend.request.headers.accept": "text/plain"
65+
}
66+
},
67+
"1 Local Function_test$ にち": {
68+
"matchCondition": {
69+
"methods": [
70+
"GET"
71+
],
72+
"route": "/MyHttpWithNonAlphanumericProxyName"
73+
},
74+
"backendUri": "https://localhost/api/Ping",
75+
"requestOverrides": {
76+
"backend.request.headers.accept": "text/plain"
77+
}
4878
}
4979
}
5080
}

test/WebJobs.Script.Tests.E2E/ProxyEndToEndTests.cs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,48 @@ public async Task LocalFunctionCall()
6969
}
7070
}
7171

72+
[Fact]
73+
[TestTrace]
74+
public async Task LocalFunctionCallForNonAlphanumericProxyName()
75+
{
76+
using (var client = CreateClient())
77+
{
78+
HttpResponseMessage response = await client.GetAsync($"MyHttpWithNonAlphanumericProxyName?code={_fixture.FunctionDefaultKey}");
79+
80+
string content = await response.Content.ReadAsStringAsync();
81+
_fixture.Assert.Equals("200", response.StatusCode.ToString("D"));
82+
_fixture.Assert.Equals("Pong", content);
83+
}
84+
}
85+
86+
[Fact]
87+
[TestTrace]
88+
public async Task CatchAllApis()
89+
{
90+
using (var client = CreateClient())
91+
{
92+
HttpResponseMessage response = await client.GetAsync($"api/blahblah?code={_fixture.FunctionDefaultKey}");
93+
94+
string content = await response.Content.ReadAsStringAsync();
95+
_fixture.Assert.Equals("200", response.StatusCode.ToString("D"));
96+
_fixture.Assert.Equals("Pong", content);
97+
}
98+
}
99+
100+
[Fact]
101+
[TestTrace]
102+
public async Task CatchAll()
103+
{
104+
using (var client = CreateClient())
105+
{
106+
HttpResponseMessage response = await client.GetAsync($"blahblah?code={_fixture.FunctionDefaultKey}");
107+
108+
string content = await response.Content.ReadAsStringAsync();
109+
_fixture.Assert.Equals("200", response.StatusCode.ToString("D"));
110+
_fixture.Assert.Equals("Pong", content);
111+
}
112+
}
113+
72114
[Fact]
73115
[TestTrace]
74116
public async Task LongRoute()

test/WebJobs.Script.Tests.E2E/WebJobs.Script.Tests.E2E.csproj

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,12 @@
107107
</ItemGroup>
108108
<ItemGroup>
109109
<None Include="app.config" />
110+
<None Include="Functions\wwwroot\PingRoute\function.json">
111+
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
112+
</None>
113+
<None Include="Functions\wwwroot\PingRoute\run.csx">
114+
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
115+
</None>
110116
<None Include="Functions\wwwroot\proxies.json">
111117
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
112118
</None>

test/WebJobs.Script.Tests/ScriptHostTests.cs

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1278,16 +1278,12 @@ public void ValidateFunctionName_ThrowsOnInvalidName(string functionName)
12781278
}
12791279

12801280
[Theory]
1281-
[InlineData("bing.com")]
1282-
[InlineData("http://bing.com")]
1283-
public void ValidateProxyName_ThrowsOnInvalidName(string proxyName)
1281+
[InlineData("myproxy")]
1282+
[InlineData("my proxy")]
1283+
[InlineData("my proxy %")]
1284+
public void UpdateProxyName(string proxyName)
12841285
{
1285-
var ex = Assert.Throws<InvalidOperationException>(() =>
1286-
{
1287-
ScriptHost.ValidateName(proxyName, true);
1288-
});
1289-
1290-
Assert.Equal(string.Format("'{0}' is not a valid proxy name.", proxyName), ex.Message);
1286+
Assert.Equal("myproxy", ScriptHost.NormalizeProxyName(proxyName));
12911287
}
12921288

12931289
[Theory]

0 commit comments

Comments
 (0)