Skip to content

Commit 02f83de

Browse files
committed
Introduced capability for host to send rawBody only.
1 parent 83157aa commit 02f83de

File tree

4 files changed

+179
-42
lines changed

4 files changed

+179
-42
lines changed

src/WebJobs.Script/Description/Rpc/WorkerLanguageInvoker.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ protected override async Task<object> InvokeCore(object[] parameters, FunctionIn
6262
ScriptInvocationContext invocationContext = new ScriptInvocationContext()
6363
{
6464
FunctionMetadata = Metadata,
65-
BindingData = context.Binder.BindingData,
65+
BindingData = context.Binder.BindingData, // This has duplicates too (of type DefaultHttpRequest). Needs to be removed after verifying this can indeed be constructed by the workers from the rest of the data being passed (https://github.com/Azure/azure-functions-host/issues/4735).
6666
ExecutionContext = context.ExecutionContext,
6767
Inputs = inputs,
6868
ResultSource = new TaskCompletionSource<ScriptInvocationResult>(),

src/WebJobs.Script/Rpc/LanguageWorkerConstants.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,5 +51,6 @@ public static class LanguageWorkerConstants
5151
// Capabilites
5252
public const string RawHttpBodyBytes = "RawHttpBodyBytes";
5353
public const string TypedDataCollection = "TypedDataCollection";
54+
public const string RpcHttpBodyOnly = "RpcHttpBodyOnly";
5455
}
5556
}

src/WebJobs.Script/Rpc/MessageExtensions/RpcMessageConversionExtensions.cs

Lines changed: 81 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -183,56 +183,94 @@ internal static TypedData ToRpcHttp(this HttpRequest request, ILogger logger, Ca
183183
// parse request body as content-type
184184
if (request.Body != null && request.ContentLength > 0)
185185
{
186-
object body = null;
187-
string rawBodyString = null;
188-
byte[] bytes = RequestBodyToBytes(request);
186+
if (IsBodyOnlySupported(capabilities))
187+
{
188+
PopulateBody(request, http, capabilities, logger);
189+
}
190+
else
191+
{
192+
PopulateBodyAndRawBody(request, http, capabilities, logger);
193+
}
194+
}
195+
196+
return typedData;
197+
}
198+
199+
private static void PopulateBody(HttpRequest request, RpcHttp http, Capabilities capabilities, ILogger logger)
200+
{
201+
object body = null;
202+
if ((MediaTypeHeaderValue.TryParse(request.ContentType, out MediaTypeHeaderValue mediaType) && IsMediaTypeOctetOrMultipart(mediaType)) || IsRawBodyBytesRequested(capabilities))
203+
{
204+
body = RequestBodyToBytes(request);
205+
}
206+
else
207+
{
208+
body = GetStringRepresentationOfBody(request);
209+
}
210+
http.Body = body.ToRpc(logger, capabilities);
211+
}
189212

190-
MediaTypeHeaderValue mediaType = null;
191-
if (MediaTypeHeaderValue.TryParse(request.ContentType, out mediaType))
213+
private static string GetStringRepresentationOfBody(HttpRequest request)
214+
{
215+
string result;
216+
using (StreamReader reader = new StreamReader(request.Body, encoding: Encoding.UTF8, detectEncodingFromByteOrderMarks: true, bufferSize: 1024, leaveOpen: true))
217+
{
218+
result = reader.ReadToEnd();
219+
}
220+
request.Body.Position = 0;
221+
return result;
222+
}
223+
224+
private static void PopulateBodyAndRawBody(HttpRequest request, RpcHttp http, Capabilities capabilities, ILogger logger)
225+
{
226+
object body = null;
227+
string rawBodyString = null;
228+
byte[] bytes = RequestBodyToBytes(request);
229+
230+
if (MediaTypeHeaderValue.TryParse(request.ContentType, out MediaTypeHeaderValue mediaType))
231+
{
232+
if (string.Equals(mediaType.MediaType, "application/json", StringComparison.OrdinalIgnoreCase))
192233
{
193-
if (string.Equals(mediaType.MediaType, "application/json", StringComparison.OrdinalIgnoreCase))
234+
rawBodyString = GetStringRepresentationOfBody(request);
235+
try
194236
{
195-
var jsonReader = new StreamReader(request.Body, Encoding.UTF8);
196-
rawBodyString = jsonReader.ReadToEnd();
197-
try
198-
{
199-
body = JsonConvert.DeserializeObject(rawBodyString);
200-
}
201-
catch (JsonException)
202-
{
203-
body = rawBodyString;
204-
}
237+
body = JsonConvert.DeserializeObject(rawBodyString);
205238
}
206-
else if (string.Equals(mediaType.MediaType, "application/octet-stream", StringComparison.OrdinalIgnoreCase) ||
207-
mediaType.MediaType.IndexOf("multipart/", StringComparison.OrdinalIgnoreCase) >= 0)
239+
catch (JsonException)
208240
{
209-
body = bytes;
210-
if (!IsRawBodyBytesRequested(capabilities))
211-
{
212-
rawBodyString = Encoding.UTF8.GetString(bytes);
213-
}
241+
body = rawBodyString;
214242
}
215243
}
216-
// default if content-tye not found or recognized
217-
if (body == null && rawBodyString == null)
244+
else if (IsMediaTypeOctetOrMultipart(mediaType))
218245
{
219-
var reader = new StreamReader(request.Body, Encoding.UTF8);
220-
body = rawBodyString = reader.ReadToEnd();
246+
body = bytes;
247+
if (!IsRawBodyBytesRequested(capabilities))
248+
{
249+
rawBodyString = Encoding.UTF8.GetString(bytes);
250+
}
221251
}
252+
}
253+
// default if content-tye not found or recognized
254+
if (body == null && rawBodyString == null)
255+
{
256+
body = rawBodyString = GetStringRepresentationOfBody(request);
257+
}
222258

223-
request.Body.Position = 0;
224-
http.Body = body.ToRpc(logger, capabilities);
225-
if (IsRawBodyBytesRequested(capabilities))
226-
{
227-
http.RawBody = bytes.ToRpc(logger, capabilities);
228-
}
229-
else
230-
{
231-
http.RawBody = rawBodyString.ToRpc(logger, capabilities);
232-
}
259+
http.Body = body.ToRpc(logger, capabilities);
260+
if (IsRawBodyBytesRequested(capabilities))
261+
{
262+
http.RawBody = bytes.ToRpc(logger, capabilities);
263+
}
264+
else
265+
{
266+
http.RawBody = rawBodyString.ToRpc(logger, capabilities);
233267
}
268+
}
234269

235-
return typedData;
270+
private static bool IsMediaTypeOctetOrMultipart(MediaTypeHeaderValue mediaType)
271+
{
272+
return mediaType != null && (string.Equals(mediaType.MediaType, "application/octet-stream", StringComparison.OrdinalIgnoreCase) ||
273+
mediaType.MediaType.IndexOf("multipart/", StringComparison.OrdinalIgnoreCase) >= 0);
236274
}
237275

238276
internal static TypedData ToRpcDefault(this object value)
@@ -314,6 +352,11 @@ private static bool IsRawBodyBytesRequested(Capabilities capabilities)
314352
return !string.IsNullOrEmpty(capabilities.GetCapabilityState(LanguageWorkerConstants.RawHttpBodyBytes));
315353
}
316354

355+
private static bool IsBodyOnlySupported(Capabilities capabilities)
356+
{
357+
return !string.IsNullOrEmpty(capabilities.GetCapabilityState(LanguageWorkerConstants.RpcHttpBodyOnly));
358+
}
359+
317360
private static bool IsTypedDataCollectionSupported(Capabilities capabilities)
318361
{
319362
return !string.IsNullOrEmpty(capabilities.GetCapabilityState(LanguageWorkerConstants.TypedDataCollection));

test/WebJobs.Script.Tests/Rpc/RpcMessageConversionExtensionsTests.cs

Lines changed: 96 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
using System.Collections.Generic;
66
using System.IO;
77
using System.Linq;
8-
using System.Reflection;
98
using System.Security.Claims;
109
using System.Text;
1110
using Google.Protobuf;
@@ -26,18 +25,112 @@ public class RpcMessageConversionExtensionsTests
2625
private static readonly string TestImageLocation = "Rpc\\Resources\\functions.png";
2726

2827
[Theory]
29-
[InlineData("application/x-www-form-urlencoded’", "say=Hi&to=Mom")]
30-
public void HttpObjects_StringBody(string expectedContentType, object body)
28+
[InlineData("application/x-www-form-urlencoded’", "say=Hi&to=Mom", true)]
29+
[InlineData("application/x-www-form-urlencoded’", "say=Hi&to=Mom", false)]
30+
public void HttpObjects_StringBody(string expectedContentType, object body, bool rcpHttpBodyOnly)
3131
{
3232
var logger = MockNullLoggerFactory.CreateLogger();
3333
var capabilities = new Capabilities(logger);
34+
if (rcpHttpBodyOnly)
35+
{
36+
capabilities.UpdateCapabilities(new MapField<string, string>
37+
{
38+
{ LanguageWorkerConstants.RpcHttpBodyOnly, rcpHttpBodyOnly.ToString() }
39+
});
40+
}
3441

3542
var headers = new HeaderDictionary();
3643
headers.Add("content-type", expectedContentType);
3744
HttpRequest request = HttpTestHelpers.CreateHttpRequest("GET", "http://localhost/api/httptrigger-scenarios", headers, body);
3845

3946
var rpcRequestObject = request.ToRpc(logger, capabilities);
4047
Assert.Equal(body.ToString(), rpcRequestObject.Http.Body.String);
48+
if (rcpHttpBodyOnly)
49+
{
50+
Assert.Equal(null, rpcRequestObject.Http.RawBody);
51+
Assert.Equal(body.ToString(), rpcRequestObject.Http.Body.String);
52+
}
53+
else
54+
{
55+
Assert.Equal(body.ToString(), rpcRequestObject.Http.RawBody.String);
56+
Assert.Equal(body.ToString(), rpcRequestObject.Http.Body.String);
57+
}
58+
59+
string contentType;
60+
rpcRequestObject.Http.Headers.TryGetValue("content-type", out contentType);
61+
Assert.Equal(expectedContentType, contentType);
62+
}
63+
64+
[Theory]
65+
[InlineData("application/json", "{\"name\":\"John\"}", true)]
66+
[InlineData("application/json", "{\"name\":\"John\"}", false)]
67+
public void HttpObjects_JsonBody(string expectedContentType, string body, bool rcpHttpBodyOnly)
68+
{
69+
var logger = MockNullLoggerFactory.CreateLogger();
70+
var capabilities = new Capabilities(logger);
71+
if (rcpHttpBodyOnly)
72+
{
73+
capabilities.UpdateCapabilities(new MapField<string, string>
74+
{
75+
{ LanguageWorkerConstants.RpcHttpBodyOnly, rcpHttpBodyOnly.ToString() }
76+
});
77+
}
78+
79+
var headers = new HeaderDictionary();
80+
headers.Add("content-type", expectedContentType);
81+
HttpRequest request = HttpTestHelpers.CreateHttpRequest("GET", "http://localhost/api/httptrigger-scenarios", headers, body);
82+
83+
var rpcRequestObject = request.ToRpc(logger, capabilities);
84+
if (rcpHttpBodyOnly)
85+
{
86+
Assert.Equal(null, rpcRequestObject.Http.RawBody);
87+
Assert.Equal(body.ToString(), rpcRequestObject.Http.Body.String);
88+
}
89+
else
90+
{
91+
Assert.Equal(body.ToString(), rpcRequestObject.Http.RawBody.String);
92+
Assert.Equal(JsonConvert.DeserializeObject(body), JsonConvert.DeserializeObject(rpcRequestObject.Http.Body.Json));
93+
}
94+
95+
string contentType;
96+
rpcRequestObject.Http.Headers.TryGetValue("content-type", out contentType);
97+
Assert.Equal(expectedContentType, contentType);
98+
}
99+
100+
[Theory]
101+
[InlineData("application/octet-stream", true)]
102+
[InlineData("application/octet-stream", false)]
103+
[InlineData("multipart/form-data; boundary=----WebKitFormBoundaryTYtz7wze2XXrH26B", true)]
104+
[InlineData("multipart/form-data; boundary=----WebKitFormBoundaryTYtz7wze2XXrH26B", false)]
105+
public void HttpTrigger_Post_ByteArray(string expectedContentType, bool rcpHttpBodyOnly)
106+
{
107+
var logger = MockNullLoggerFactory.CreateLogger();
108+
var capabilities = new Capabilities(logger);
109+
if (rcpHttpBodyOnly)
110+
{
111+
capabilities.UpdateCapabilities(new MapField<string, string>
112+
{
113+
{ LanguageWorkerConstants.RpcHttpBodyOnly, rcpHttpBodyOnly.ToString() }
114+
});
115+
}
116+
117+
var headers = new HeaderDictionary();
118+
headers.Add("content-type", expectedContentType);
119+
byte[] body = new byte[] { 1, 2, 3, 4, 5 };
120+
121+
HttpRequest request = HttpTestHelpers.CreateHttpRequest("POST", "http://localhost/api/httptrigger-scenarios", headers, body);
122+
123+
var rpcRequestObject = request.ToRpc(logger, capabilities);
124+
if (rcpHttpBodyOnly)
125+
{
126+
Assert.Equal(null, rpcRequestObject.Http.RawBody);
127+
Assert.Equal(body, rpcRequestObject.Http.Body.Bytes);
128+
}
129+
else
130+
{
131+
Assert.Equal(body, rpcRequestObject.Http.Body.Bytes);
132+
Assert.Equal(Encoding.UTF8.GetString(body), rpcRequestObject.Http.RawBody.String);
133+
}
41134

42135
string contentType;
43136
rpcRequestObject.Http.Headers.TryGetValue("content-type", out contentType);

0 commit comments

Comments
 (0)