Skip to content

Commit b4f6c80

Browse files
committed
Handle duplicate query strings (#1154)
1 parent 491f08b commit b4f6c80

File tree

10 files changed

+129
-29
lines changed

10 files changed

+129
-29
lines changed

src/WebJobs.Script.WebHost/Filters/AuthorizationLevelAttribute.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ internal static async Task<AuthorizationLevel> GetAuthorizationLevelAsync(HttpRe
6464
}
6565
else
6666
{
67-
var queryParameters = request.GetQueryNameValuePairs().ToDictionary(p => p.Key, p => p.Value, StringComparer.OrdinalIgnoreCase);
67+
var queryParameters = request.GetQueryParameterDictionary();
6868
queryParameters.TryGetValue("code", out keyValue);
6969
}
7070

src/WebJobs.Script/Description/Node/NodeFunctionInvoker.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -434,7 +434,7 @@ private static Dictionary<string, object> CreateRequestObject(HttpRequestMessage
434434
Dictionary<string, object> requestObject = new Dictionary<string, object>();
435435
requestObject["originalUrl"] = request.RequestUri.ToString();
436436
requestObject["method"] = request.Method.ToString().ToUpperInvariant();
437-
requestObject["query"] = request.GetQueryNameValuePairs().ToDictionary(kv => kv.Key, kv => kv.Value, StringComparer.OrdinalIgnoreCase);
437+
requestObject["query"] = request.GetQueryParameterDictionary();
438438

439439
// since HTTP headers are case insensitive, we lower-case the keys
440440
// as does Node.js request object

src/WebJobs.Script/Description/ScriptFunctionInvokerBase.cs

Lines changed: 30 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -154,35 +154,41 @@ protected virtual void InitializeEnvironmentVariables(Dictionary<string, string>
154154
environmentVariables[outputBinding.Metadata.Name] = Path.Combine(functionInstanceOutputPath, outputBinding.Metadata.Name);
155155
}
156156

157-
Type triggerParameterType = input.GetType();
158-
if (triggerParameterType == typeof(HttpRequestMessage))
157+
var request = input as HttpRequestMessage;
158+
if (request != null)
159159
{
160-
HttpRequestMessage request = (HttpRequestMessage)input;
161-
environmentVariables["REQ_METHOD"] = request.Method.ToString();
160+
InitializeHttpRequestEnvironmentVariables(environmentVariables, request);
161+
}
162+
}
162163

163-
Dictionary<string, string> queryParams = request.GetQueryNameValuePairs().ToDictionary(p => p.Key, p => p.Value, StringComparer.OrdinalIgnoreCase);
164-
foreach (var queryParam in queryParams)
165-
{
166-
string varName = string.Format(CultureInfo.InvariantCulture, "REQ_QUERY_{0}", queryParam.Key.ToUpperInvariant());
167-
environmentVariables[varName] = queryParam.Value;
168-
}
164+
internal static void InitializeHttpRequestEnvironmentVariables(Dictionary<string, string> environmentVariables, HttpRequestMessage request)
165+
{
166+
environmentVariables["REQ_ORIGINAL_URL"] = request.RequestUri.ToString();
167+
environmentVariables["REQ_METHOD"] = request.Method.ToString();
168+
environmentVariables["REQ_QUERY"] = request.RequestUri.Query;
169169

170-
var headers = request.GetRawHeaders();
171-
foreach (var header in headers)
172-
{
173-
string varName = string.Format(CultureInfo.InvariantCulture, "REQ_HEADERS_{0}", header.Key.ToUpperInvariant());
174-
environmentVariables[varName] = header.Value;
175-
}
170+
var queryParams = request.GetQueryParameterDictionary();
171+
foreach (var queryParam in queryParams)
172+
{
173+
string varName = string.Format(CultureInfo.InvariantCulture, "REQ_QUERY_{0}", queryParam.Key.ToUpperInvariant());
174+
environmentVariables[varName] = queryParam.Value;
175+
}
176+
177+
var headers = request.GetRawHeaders();
178+
foreach (var header in headers)
179+
{
180+
string varName = string.Format(CultureInfo.InvariantCulture, "REQ_HEADERS_{0}", header.Key.ToUpperInvariant());
181+
environmentVariables[varName] = header.Value;
182+
}
176183

177-
object value = null;
178-
if (request.Properties.TryGetValue(ScriptConstants.AzureFunctionsHttpRouteDataKey, out value))
184+
object value = null;
185+
if (request.Properties.TryGetValue(ScriptConstants.AzureFunctionsHttpRouteDataKey, out value))
186+
{
187+
Dictionary<string, object> routeBindingData = (Dictionary<string, object>)value;
188+
foreach (var pair in routeBindingData)
179189
{
180-
Dictionary<string, object> routeBindingData = (Dictionary<string, object>)value;
181-
foreach (var pair in routeBindingData)
182-
{
183-
string varName = string.Format(CultureInfo.InvariantCulture, "REQ_PARAMS_{0}", pair.Key.ToUpperInvariant());
184-
environmentVariables[varName] = pair.Value.ToString();
185-
}
190+
string varName = string.Format(CultureInfo.InvariantCulture, "REQ_PARAMS_{0}", pair.Key.ToUpperInvariant());
191+
environmentVariables[varName] = pair.Value.ToString();
186192
}
187193
}
188194
}

src/WebJobs.Script/Extensions/HttpRequestMessageExtensions.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,22 @@
33

44
using System;
55
using System.Collections.Generic;
6+
using System.Linq;
67
using System.Net.Http;
78

89
namespace Microsoft.Azure.WebJobs.Script
910
{
1011
public static class HttpRequestMessageExtensions
1112
{
13+
public static IDictionary<string, string> GetQueryParameterDictionary(this HttpRequestMessage request)
14+
{
15+
var keyValuePairs = request.GetQueryNameValuePairs();
16+
17+
// last one wins for any duplicate query parameters
18+
return keyValuePairs.GroupBy(p => p.Key, StringComparer.OrdinalIgnoreCase)
19+
.ToDictionary(p => p.Key, s => s.Last().Value, StringComparer.OrdinalIgnoreCase);
20+
}
21+
1222
public static IDictionary<string, string> GetRawHeaders(this HttpRequestMessage request)
1323
{
1424
Dictionary<string, string> headers = new Dictionary<string, string>();

test/WebJobs.Script.Tests.Integration/SamplesEndToEndTests.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,20 @@ public async Task HttpTrigger_Get_Succeeds()
278278
Assert.Equal("Hello Mathew", body);
279279
}
280280

281+
[Fact]
282+
public async Task HttpTrigger_DuplicateQueryParams_Succeeds()
283+
{
284+
string uri = "api/httptrigger?code=hyexydhln844f2mb7hgsup2yf8dowlb0885mbiq1&name=Mathew&name=Amy";
285+
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, uri);
286+
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/plain"));
287+
288+
HttpResponseMessage response = await this._fixture.HttpClient.SendAsync(request);
289+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
290+
string body = await response.Content.ReadAsStringAsync();
291+
Assert.Equal("text/plain", response.Content.Headers.ContentType.MediaType);
292+
Assert.Equal("Hello Amy", body);
293+
}
294+
281295
[Fact]
282296
public async Task HttpTrigger_CustomRoute_Get_ReturnsExpectedResponse()
283297
{

test/WebJobs.Script.Tests.Integration/TestScripts/WindowsBatch/HttpTrigger/run.bat

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
echo OFF
22

3+
IF DEFINED req_query_details (
4+
echo URL = "%req_original_url%" >> %res%
5+
echo Query = "%req_query%" >> %res%
6+
)
7+
38
IF DEFINED req_headers_test-header (
49
echo test-header = %req_headers_test-header% >> %res%
510
)

test/WebJobs.Script.Tests.Integration/WindowsBatchEndToEndTests.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ public async Task HttpTrigger_Get()
7070
string testData = Guid.NewGuid().ToString();
7171
HttpRequestMessage request = new HttpRequestMessage
7272
{
73-
RequestUri = new Uri(string.Format("http://localhost/api/httptrigger?value={0}", testData)),
73+
RequestUri = new Uri($"http://localhost/api/httptrigger?value={testData}&a=one&a=two&b=three&details=1"),
7474
Method = HttpMethod.Get
7575
};
7676
request.SetConfiguration(Fixture.RequestConfiguration);
@@ -88,8 +88,10 @@ public async Task HttpTrigger_Get()
8888

8989
string body = await response.Content.ReadAsStringAsync();
9090
string[] lines = body.Split(new string[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);
91-
Assert.Equal("test-header = Test Request Header", lines[0].Trim());
92-
Assert.Equal(string.Format("Value = {0}", testData), lines[1].Trim());
91+
Assert.Equal($"URL = \"http://localhost/api/httptrigger?value={testData}&a=one&a=two&b=three&details=1\"", lines[0].Trim());
92+
Assert.Equal($"Query = \"?value={testData}&a=one&a=two&b=three&details=1\"", lines[1].Trim());
93+
Assert.Equal("test-header = Test Request Header", lines[2].Trim());
94+
Assert.Equal($"Value = {testData}", lines[3].Trim());
9395

9496
request.RequestUri = new Uri(string.Format("http://localhost/api/httptrigger", testData));
9597
request.Headers.Clear();
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
using System.Collections.Generic;
5+
using System.Net.Http;
6+
using Microsoft.Azure.WebJobs.Script.Description;
7+
using Xunit;
8+
9+
namespace Microsoft.Azure.WebJobs.Script.Tests.Description.Script
10+
{
11+
public class ScriptFunctionInvokerBaseTests
12+
{
13+
[Fact]
14+
public void InitializeHttpRequestEnvironmentVariables_SetsExpectedVariables()
15+
{
16+
var environmentVariables = new Dictionary<string, string>();
17+
var request = new HttpRequestMessage(HttpMethod.Get, "http://test.com/test?a=1&b=2&b=3&c=4");
18+
request.Headers.Add("TEST-A", "a");
19+
request.Headers.Add("TEST-B", "b");
20+
21+
var routeData = new Dictionary<string, object>
22+
{
23+
{ "a", 123 },
24+
{ "b", 456 }
25+
};
26+
request.Properties.Add(ScriptConstants.AzureFunctionsHttpRouteDataKey, routeData);
27+
28+
ScriptFunctionInvokerBase.InitializeHttpRequestEnvironmentVariables(environmentVariables, request);
29+
Assert.Equal(10, environmentVariables.Count);
30+
31+
// verify base request properties
32+
Assert.Equal(request.RequestUri.ToString(), environmentVariables["REQ_ORIGINAL_URL"]);
33+
Assert.Equal(request.Method.ToString(), environmentVariables["REQ_METHOD"]);
34+
Assert.Equal(request.RequestUri.Query.ToString(), environmentVariables["REQ_QUERY"]);
35+
36+
// verify query parameters
37+
Assert.Equal("1", environmentVariables["REQ_QUERY_A"]);
38+
Assert.Equal("3", environmentVariables["REQ_QUERY_B"]);
39+
Assert.Equal("4", environmentVariables["REQ_QUERY_C"]);
40+
41+
// verify headers
42+
Assert.Equal("a", environmentVariables["REQ_HEADERS_TEST-A"]);
43+
Assert.Equal("b", environmentVariables["REQ_HEADERS_TEST-B"]);
44+
45+
// verify route parameters
46+
Assert.Equal("123", environmentVariables["REQ_PARAMS_A"]);
47+
Assert.Equal("456", environmentVariables["REQ_PARAMS_B"]);
48+
}
49+
}
50+
}

test/WebJobs.Script.Tests/Extensions/HttpRequestMessageExtensions.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,18 @@ namespace Microsoft.Azure.WebJobs.Script.Tests
88
{
99
public class HttpRequestMessageExtensions
1010
{
11+
[Fact]
12+
public void GetQueryParameterDictionary_ReturnsExpectedParameters()
13+
{
14+
var request = new HttpRequestMessage(HttpMethod.Get, "http://test.com/test?a=1&b=2&b=3&c=4&c=5&d=6");
15+
var parameters = request.GetQueryParameterDictionary();
16+
Assert.Equal(4, parameters.Count);
17+
Assert.Equal("1", parameters["a"]);
18+
Assert.Equal("3", parameters["b"]);
19+
Assert.Equal("5", parameters["c"]);
20+
Assert.Equal("6", parameters["d"]);
21+
}
22+
1123
[Fact]
1224
public void GetRawHeaders_ReturnsExpectedHeaders()
1325
{

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,7 @@
440440
<Compile Include="Binding\HttpBindingTests.cs" />
441441
<Compile Include="Controllers\Admin\AdminControllerTests.cs" />
442442
<Compile Include="Description\DotNet\CSharp\CSharpCompilationServiceTests.cs" />
443+
<Compile Include="Description\Script\ScriptFunctionInvokerBaseTests.cs" />
443444
<Compile Include="Filters\AuthorizationLevelAttributeTests.cs" />
444445
<Compile Include="Binding\ExtensionBindingTests.cs" />
445446
<Compile Include="Diagnostics\CompositeTraceWriterTests.cs" />

0 commit comments

Comments
 (0)