Skip to content

Commit 073bc5d

Browse files
authored
Fix httpRequestUri and populate body (#6308)
Fixed forwarding of http requests.
1 parent cd026ce commit 073bc5d

File tree

8 files changed

+235
-48
lines changed

8 files changed

+235
-48
lines changed

src/WebJobs.Script/Extensions/HttpRequestExtensions.cs

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,13 @@
55
using System.Collections.Generic;
66
using System.IO;
77
using System.Linq;
8+
using System.Net.Http;
89
using System.Net.Http.Headers;
910
using System.Security.Claims;
10-
using System.Text;
1111
using System.Threading.Tasks;
1212
using Microsoft.AspNetCore.Http;
1313
using Microsoft.AspNetCore.Http.Extensions;
14+
using Microsoft.AspNetCore.WebUtilities;
1415
using Microsoft.Azure.WebJobs.Extensions.Http;
1516
using Microsoft.Extensions.Primitives;
1617
using Newtonsoft.Json;
@@ -122,6 +123,34 @@ public static bool IsMediaTypeOctetOrMultipart(this HttpRequest request)
122123
return false;
123124
}
124125

126+
public static HttpRequestMessage ToHttpRequestMessage(this HttpRequest request, string requestUri)
127+
{
128+
HttpRequestMessage httpRequest = new HttpRequestMessage
129+
{
130+
RequestUri = new Uri(QueryHelpers.AddQueryString(requestUri, request.GetQueryCollectionAsDictionary()))
131+
};
132+
133+
foreach (var header in request.Headers)
134+
{
135+
httpRequest.Headers.TryAddWithoutValidation(header.Key, header.Value.AsEnumerable());
136+
}
137+
138+
httpRequest.Method = new HttpMethod(request.Method);
139+
140+
// Copy body
141+
if (request.ContentLength != null && request.ContentLength > 0)
142+
{
143+
httpRequest.Content = new StreamContent(request.Body);
144+
httpRequest.Content.Headers.Add("Content-Length", request.ContentLength.ToString());
145+
if (!string.IsNullOrEmpty(request.ContentType))
146+
{
147+
httpRequest.Content.Headers.Add("Content-Type", request.ContentType);
148+
}
149+
}
150+
151+
return httpRequest;
152+
}
153+
125154
public static async Task<JObject> GetRequestAsJObject(this HttpRequest request)
126155
{
127156
var jObjectHttp = new JObject();

src/WebJobs.Script/Workers/Http/DefaultHttpWorkerService.cs

Lines changed: 42 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,10 @@
55
using System.Collections.Generic;
66
using System.Linq;
77
using System.Net.Http;
8-
using System.Net.Http.Formatting;
98
using System.Net.Sockets;
109
using System.Threading;
1110
using System.Threading.Tasks;
1211
using Microsoft.AspNetCore.Http;
13-
using Microsoft.AspNetCore.WebUtilities;
1412
using Microsoft.Azure.WebJobs.Script.Description;
1513
using Microsoft.Azure.WebJobs.Script.Extensions;
1614
using Microsoft.Extensions.Logging;
@@ -31,9 +29,9 @@ public DefaultHttpWorkerService(IOptions<HttpWorkerOptions> httpWorkerOptions, I
3129

3230
internal DefaultHttpWorkerService(HttpClient httpClient, IOptions<HttpWorkerOptions> httpWorkerOptions, ILogger logger)
3331
{
34-
_httpClient = httpClient;
35-
_httpWorkerOptions = httpWorkerOptions.Value;
36-
_logger = logger;
32+
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
33+
_httpWorkerOptions = httpWorkerOptions.Value ?? throw new ArgumentNullException(nameof(httpWorkerOptions.Value));
34+
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
3735
}
3836

3937
public Task InvokeAsync(ScriptInvocationContext scriptInvocationContext)
@@ -69,11 +67,17 @@ internal async Task ProcessHttpInAndOutInvocationRequest(ScriptInvocationContext
6967

7068
try
7169
{
72-
using (HttpRequestMessage httpRequestMessage = CreateAndGetHttpRequestMessage(scriptInvocationContext.FunctionMetadata.Name, scriptInvocationContext.ExecutionContext.InvocationId.ToString(), new HttpMethod(httpRequest.Method), httpRequest.GetQueryCollectionAsDictionary()))
70+
string uriPathValue = GetPathValue(_httpWorkerOptions, scriptInvocationContext.FunctionMetadata.Name, httpRequest);
71+
string uri = BuildAndGetUri(uriPathValue);
72+
73+
using (HttpRequestMessage httpRequestMessage = httpRequest.ToHttpRequestMessage(uri))
7374
{
74-
_logger.LogDebug("Sending http request message for simple httpTrigger function: '{functionName}' invocationId: '{invocationId}'", scriptInvocationContext.FunctionMetadata.Name, scriptInvocationContext.ExecutionContext.InvocationId);
75+
AddHeaders(httpRequestMessage, scriptInvocationContext.ExecutionContext.InvocationId.ToString());
76+
77+
_logger.LogDebug("Forwarding http request for httpTrigger function: '{functionName}' invocationId: '{invocationId}'", scriptInvocationContext.FunctionMetadata.Name, scriptInvocationContext.ExecutionContext.InvocationId);
7578
HttpResponseMessage invocationResponse = await _httpClient.SendAsync(httpRequestMessage);
76-
_logger.LogDebug("Received http response for simple httpTrigger function: '{functionName}' invocationId: '{invocationId}'", scriptInvocationContext.FunctionMetadata.Name, scriptInvocationContext.ExecutionContext.InvocationId);
79+
_logger.LogDebug("Received http response for httpTrigger function: '{functionName}' invocationId: '{invocationId}'", scriptInvocationContext.FunctionMetadata.Name, scriptInvocationContext.ExecutionContext.InvocationId);
80+
7781
BindingMetadata httpOutputBinding = scriptInvocationContext.FunctionMetadata.OutputBindings.FirstOrDefault();
7882
if (httpOutputBinding != null)
7983
{
@@ -91,18 +95,39 @@ internal async Task ProcessHttpInAndOutInvocationRequest(ScriptInvocationContext
9195
}
9296
}
9397

98+
internal void AddHeaders(HttpRequestMessage httpRequest, string invocationId)
99+
{
100+
httpRequest.Headers.Add(HttpWorkerConstants.HostVersionHeaderName, ScriptHost.Version);
101+
httpRequest.Headers.Add(HttpWorkerConstants.InvocationIdHeaderName, invocationId);
102+
httpRequest.Headers.UserAgent.ParseAdd($"{HttpWorkerConstants.UserAgentHeaderValue}/{ScriptHost.Version}");
103+
}
104+
105+
internal string GetPathValue(HttpWorkerOptions httpWorkerOptions, string functionName, HttpRequest httpRequest)
106+
{
107+
string pathValue = functionName;
108+
if (httpWorkerOptions.EnableForwardingHttpRequest && httpWorkerOptions.Type == CustomHandlerType.Http)
109+
{
110+
pathValue = httpRequest.GetRequestUri().AbsolutePath;
111+
}
112+
113+
return pathValue;
114+
}
115+
94116
internal async Task ProcessDefaultInvocationRequest(ScriptInvocationContext scriptInvocationContext)
95117
{
96118
try
97119
{
98120
HttpScriptInvocationContext httpScriptInvocationContext = await scriptInvocationContext.ToHttpScriptInvocationContext();
121+
string uri = BuildAndGetUri(scriptInvocationContext.FunctionMetadata.Name);
122+
99123
// Build httpRequestMessage from scriptInvocationContext
100-
using (HttpRequestMessage httpRequestMessage = CreateAndGetHttpRequestMessage(scriptInvocationContext.FunctionMetadata.Name, scriptInvocationContext.ExecutionContext.InvocationId.ToString(), HttpMethod.Post))
124+
using (HttpRequestMessage httpRequestMessage = httpScriptInvocationContext.ToHttpRequestMessage(uri))
101125
{
102-
httpRequestMessage.Content = new ObjectContent<HttpScriptInvocationContext>(httpScriptInvocationContext, new JsonMediaTypeFormatter());
126+
AddHeaders(httpRequestMessage, scriptInvocationContext.ExecutionContext.InvocationId.ToString());
127+
103128
_logger.LogDebug("Sending http request for function:{functionName} invocationId:{invocationId}", scriptInvocationContext.FunctionMetadata.Name, scriptInvocationContext.ExecutionContext.InvocationId);
104129
HttpResponseMessage response = await _httpClient.SendAsync(httpRequestMessage);
105-
_logger.LogDebug("Received http request for function:{functionName} invocationId:{invocationId}", scriptInvocationContext.FunctionMetadata.Name, scriptInvocationContext.ExecutionContext.InvocationId);
130+
_logger.LogDebug("Received http response for function:{functionName} invocationId:{invocationId}", scriptInvocationContext.FunctionMetadata.Name, scriptInvocationContext.ExecutionContext.InvocationId);
106131

107132
// Only process output bindings if response is succeess code
108133
response.EnsureSuccessStatusCode();
@@ -113,11 +138,11 @@ internal async Task ProcessDefaultInvocationRequest(ScriptInvocationContext scri
113138
{
114139
if (httpScriptInvocationResult.Outputs == null || !httpScriptInvocationResult.Outputs.Any())
115140
{
116-
_logger.LogWarning("Outputs not set on http response for invocationId:{invocationId}", scriptInvocationContext.ExecutionContext.InvocationId);
141+
_logger.LogWarning("Outputs not set on http response for invocationId:{invocationId}", scriptInvocationContext.ExecutionContext.InvocationId);
117142
}
118143
if (httpScriptInvocationResult.ReturnValue == null)
119144
{
120-
_logger.LogWarning("ReturnValue not set on http response for invocationId:{invocationId}", scriptInvocationContext.ExecutionContext.InvocationId);
145+
_logger.LogWarning("ReturnValue not set on http response for invocationId:{invocationId}", scriptInvocationContext.ExecutionContext.InvocationId);
121146
}
122147

123148
ProcessLogsFromHttpResponse(scriptInvocationContext, httpScriptInvocationResult);
@@ -156,32 +181,6 @@ internal void ProcessLogsFromHttpResponse(ScriptInvocationContext scriptInvocati
156181
}
157182
}
158183

159-
private HttpRequestMessage CreateAndGetHttpRequestMessage(string functionName, string invocationId, HttpMethod requestMethod, IDictionary<string, string> queryCollectionAsDictionary = null)
160-
{
161-
var requestMessage = new HttpRequestMessage();
162-
AddRequestHeadersAndSetRequestUri(requestMessage, functionName, invocationId);
163-
if (queryCollectionAsDictionary != null)
164-
{
165-
requestMessage.RequestUri = new Uri(QueryHelpers.AddQueryString(requestMessage.RequestUri.ToString(), queryCollectionAsDictionary));
166-
}
167-
requestMessage.Method = requestMethod;
168-
return requestMessage;
169-
}
170-
171-
private void AddRequestHeadersAndSetRequestUri(HttpRequestMessage httpRequestMessage, string functionName, string invocationId)
172-
{
173-
string pathValue = functionName;
174-
// _httpWorkerOptions.Type is set to None only in HttpWorker section
175-
if (httpRequestMessage.RequestUri != null && _httpWorkerOptions.Type != CustomHandlerType.None)
176-
{
177-
pathValue = httpRequestMessage.RequestUri.AbsolutePath;
178-
}
179-
httpRequestMessage.RequestUri = new Uri(BuildAndGetUri(pathValue));
180-
httpRequestMessage.Headers.Add(HttpWorkerConstants.InvocationIdHeaderName, invocationId);
181-
httpRequestMessage.Headers.Add(HttpWorkerConstants.HostVersionHeaderName, ScriptHost.Version);
182-
httpRequestMessage.Headers.UserAgent.ParseAdd($"{HttpWorkerConstants.UserAgentHeaderValue}/{ScriptHost.Version}");
183-
}
184-
185184
public async Task<bool> IsWorkerReady(CancellationToken cancellationToken)
186185
{
187186
bool continueWaitingForWorker = await Utility.DelayAsync(WorkerConstants.WorkerInitTimeoutSeconds, WorkerConstants.WorkerReadyCheckPollingIntervalMilliseconds, async () =>
@@ -213,13 +212,13 @@ private async Task<bool> IsWorkerReadyForRequest()
213212
}
214213
}
215214

216-
private string BuildAndGetUri(string pathValue = null)
215+
internal string BuildAndGetUri(string pathValue = null)
217216
{
218-
if (!string.IsNullOrEmpty(pathValue))
217+
if (string.IsNullOrEmpty(pathValue))
219218
{
220-
return new UriBuilder(WorkerConstants.HttpScheme, WorkerConstants.HostName, _httpWorkerOptions.Port, pathValue).ToString();
219+
return new UriBuilder(WorkerConstants.HttpScheme, WorkerConstants.HostName, _httpWorkerOptions.Port).ToString();
221220
}
222-
return new UriBuilder(WorkerConstants.HttpScheme, WorkerConstants.HostName, _httpWorkerOptions.Port).ToString();
221+
return new UriBuilder(WorkerConstants.HttpScheme, WorkerConstants.HostName, _httpWorkerOptions.Port, pathValue).ToString();
223222
}
224223

225224
private async Task<HttpResponseMessage> SendRequest(string requestUri, HttpMethod method = null)
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
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;
5+
using System.Net.Http;
6+
using System.Net.Http.Formatting;
7+
using Microsoft.Azure.WebJobs.Script.Workers.Http;
8+
9+
namespace Microsoft.Azure.WebJobs.Script.Workers
10+
{
11+
public static class HttpScriptInvocationContextExtensions
12+
{
13+
public static HttpRequestMessage ToHttpRequestMessage(this HttpScriptInvocationContext context, string requestUri)
14+
{
15+
HttpRequestMessage httpRequest = new HttpRequestMessage
16+
{
17+
RequestUri = new Uri(requestUri),
18+
Method = HttpMethod.Post,
19+
Content = new ObjectContent<HttpScriptInvocationContext>(context, new JsonMediaTypeFormatter())
20+
};
21+
22+
return httpRequest;
23+
}
24+
}
25+
}

test/WebJobs.Script.Tests.Integration/WebHostEndToEnd/SamplesEndToEndTests_CustomHandler.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ private async Task InvokeHttpTrigger(string functionName)
5252

5353
string responseContent = await response.Content.ReadAsStringAsync();
5454
JObject res = JObject.Parse(responseContent);
55-
Assert.True(res["functionName"].ToString().StartsWith(functionName));
55+
Assert.True(res["functionName"].ToString().StartsWith($"api/{functionName}"));
5656
}
5757

5858
private async Task InvokeProxy()

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

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@
33

44
using System;
55
using System.Collections.Generic;
6+
using System.Linq;
7+
using System.Net.Http;
68
using System.Security.Claims;
79
using System.Security.Principal;
8-
using System.Text;
10+
using System.Threading.Tasks;
911
using Microsoft.AspNetCore.Http;
10-
using Microsoft.Azure.WebJobs.Script.Config;
12+
using Microsoft.AspNetCore.WebUtilities;
1113
using Microsoft.Azure.WebJobs.Script.Extensions;
14+
using Microsoft.Azure.WebJobs.Script.Tests.HttpWorker;
1215
using Microsoft.WebJobs.Script.Tests;
1316
using Xunit;
1417

@@ -54,6 +57,38 @@ public void ConvertUserIdentitiesToString_RemovesCircularReference()
5457
string userIdentitiesString = HttpRequestExtensions.GetUserIdentitiesAsString(claimsIdentities);
5558
Assert.Contains(expectedUserIdentities, userIdentitiesString);
5659
}
60+
61+
[Fact]
62+
public async Task GetHttpRequest_HasAllHeadersMethodAndBody()
63+
{
64+
string requestUri = "http://localhost";
65+
HttpRequest testRequest = HttpWorkerTestUtilities.GetTestHttpRequest();
66+
Uri expectedUri = new Uri(QueryHelpers.AddQueryString(requestUri, testRequest.GetQueryCollectionAsDictionary()));
67+
HttpRequestMessage clonedRequest = testRequest.ToHttpRequestMessage(requestUri);
68+
Assert.Equal(testRequest.Headers.Count, clonedRequest.Headers.Count() + 1); // Content-Length would go to content header
69+
70+
foreach (var header in testRequest.Headers)
71+
{
72+
IEnumerable<string> actualHeaderValue = header.Value.AsEnumerable();
73+
IEnumerable<string> clonedHeaderValue;
74+
75+
if (!header.Key.Equals("Content-Type", StringComparison.OrdinalIgnoreCase) && !header.Key.Equals("Content-Length", StringComparison.OrdinalIgnoreCase))
76+
{
77+
clonedHeaderValue = clonedRequest.Headers.GetValues(header.Key);
78+
}
79+
else
80+
{
81+
clonedHeaderValue = clonedRequest.Content.Headers.GetValues(header.Key);
82+
}
83+
84+
var count = actualHeaderValue.Except(clonedHeaderValue).Count();
85+
Assert.Equal(count, 0);
86+
}
87+
88+
Assert.Equal(testRequest.Method, clonedRequest.Method.ToString());
89+
Assert.Equal(clonedRequest.RequestUri.ToString(), expectedUri.ToString());
90+
Assert.Equal(await clonedRequest.Content.ReadAsStringAsync(), "\"hello world\"");
91+
}
5792
}
5893

5994
internal class TestIdentity : IIdentity
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
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;
5+
using System.Collections.Generic;
6+
using System.Linq;
7+
using System.Net.Http;
8+
using System.Net.Http.Formatting;
9+
using System.Threading.Tasks;
10+
using Microsoft.Azure.WebJobs.Script.Workers;
11+
using Microsoft.Azure.WebJobs.Script.Workers.Http;
12+
using Newtonsoft.Json;
13+
using Xunit;
14+
15+
namespace Microsoft.Azure.WebJobs.Script.Tests
16+
{
17+
public class HttpScriptInvocationContextExtensionTests
18+
{
19+
[Fact]
20+
public async Task ToHttpRequestTest()
21+
{
22+
HttpScriptInvocationContext testContext = new HttpScriptInvocationContext
23+
{
24+
Data = new Dictionary<string, object>(),
25+
Metadata = new Dictionary<string, object>(),
26+
};
27+
testContext.Data.Add("randomDataKey", "randomDataValue");
28+
testContext.Metadata.Add("randomMetaDataKey", "randomMetaDataValue");
29+
30+
string expectedUri = "http://randomhost";
31+
HttpRequestMessage result = testContext.ToHttpRequestMessage(expectedUri);
32+
33+
string resultContent = await result.Content.ReadAsStringAsync();
34+
HttpScriptInvocationContext expected = JsonConvert.DeserializeObject<HttpScriptInvocationContext>(resultContent);
35+
36+
Assert.Equal(result.RequestUri.ToString(), new Uri(expectedUri).ToString());
37+
Assert.Equal(result.Method, HttpMethod.Post);
38+
foreach (string key in testContext.Data.Keys)
39+
{
40+
Assert.Equal(testContext.Data[key], expected.Data[key]);
41+
}
42+
foreach (string key in testContext.Metadata.Keys)
43+
{
44+
Assert.Equal(testContext.Metadata[key], expected.Metadata[key]);
45+
}
46+
}
47+
}
48+
}

0 commit comments

Comments
 (0)