Skip to content

Commit 940fb13

Browse files
committed
Adding flag to enable scripting languages to write a raw HTTP response
1 parent 9fce99c commit 940fb13

File tree

6 files changed

+93
-11
lines changed

6 files changed

+93
-11
lines changed

src/WebJobs.Script/Binding/HttpBinding.cs

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ public override async Task BindAsync(BindingContext context)
4848

4949
HttpStatusCode statusCode = HttpStatusCode.OK;
5050
JObject headers = null;
51+
bool isRawResponse = false;
5152
if (content is string)
5253
{
5354
try
@@ -83,6 +84,12 @@ public override async Task BindAsync(BindingContext context)
8384
{
8485
statusCode = (HttpStatusCode)(int)value;
8586
}
87+
88+
if ((jo.TryGetValue("isRaw", StringComparison.OrdinalIgnoreCase, out value) && value is JValue) &&
89+
value.Type == JTokenType.Boolean)
90+
{
91+
isRawResponse = (bool)value;
92+
}
8693
}
8794
}
8895
catch (JsonException)
@@ -91,7 +98,7 @@ public override async Task BindAsync(BindingContext context)
9198
}
9299
}
93100

94-
HttpResponseMessage response = CreateResponse(request, statusCode, content, headers);
101+
HttpResponseMessage response = CreateResponse(request, statusCode, content, headers, isRawResponse);
95102

96103
if (headers != null)
97104
{
@@ -105,8 +112,15 @@ public override async Task BindAsync(BindingContext context)
105112
request.Properties[ScriptConstants.AzureFunctionsHttpResponseKey] = response;
106113
}
107114

108-
private static HttpResponseMessage CreateResponse(HttpRequestMessage request, HttpStatusCode statusCode, object content, JObject headers)
115+
private static HttpResponseMessage CreateResponse(HttpRequestMessage request, HttpStatusCode statusCode, object content, JObject headers, bool isRawResponse)
109116
{
117+
if (isRawResponse)
118+
{
119+
// We only write the response through one of the formatters if
120+
// the function hasn't indicated that it wants to write the raw response
121+
return new HttpResponseMessage(statusCode) { Content = CreateResultContent(content) };
122+
}
123+
110124
JToken contentType = null;
111125
MediaTypeHeaderValue mediaType = null;
112126
if (content != null &&
@@ -138,13 +152,9 @@ private static HttpResponseMessage CreateResponse(HttpRequestMessage request, Ht
138152
return CreateNegotiatedResponse(request, statusCode, content);
139153
}
140154

141-
private static HttpContent CreateResultContent(object content, string mediaType)
155+
private static HttpContent CreateResultContent(object content, string mediaType = null)
142156
{
143-
if (content is string)
144-
{
145-
return new StringContent((string)content, null, mediaType);
146-
}
147-
else if (content is byte[])
157+
if (content is byte[])
148158
{
149159
return new ByteArrayContent((byte[])content);
150160
}
@@ -153,7 +163,7 @@ private static HttpContent CreateResultContent(object content, string mediaType)
153163
return new StreamContent((Stream)content);
154164
}
155165

156-
return null;
166+
return new StringContent(content?.ToString() ?? string.Empty, null, mediaType);
157167
}
158168

159169
private static HttpResponseMessage CreateNegotiatedResponse(HttpRequestMessage request, HttpStatusCode statusCode, object content)

src/WebJobs.Script/Content/Script/http/response.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ module.exports = (context) => {
3737
.send(body);
3838
},
3939

40+
raw: (body) => {
41+
res.isRaw = true;
42+
return res.send(body);
43+
},
44+
4045
get: (field) => {
4146
return res.headers[field]
4247
}

src/WebJobs.Script/GlobalSuppressions.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,4 +108,6 @@
108108
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Scope = "member", Target = "Microsoft.Azure.WebJobs.Script.Description.FunctionLoader`1.#.ctor(System.Func`2<System.Threading.CancellationToken,System.Threading.Tasks.Task`1<!0>>)")]
109109
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Scope = "member", Target = "Microsoft.Azure.WebJobs.Script.Description.FunctionLoader`1.#Reset()")]
110110
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Scope = "member", Target = "Microsoft.Azure.WebJobs.Script.Binding.HttpBinding.#CreateResponse(System.Net.Http.HttpRequestMessage,System.Net.HttpStatusCode,System.Object,Newtonsoft.Json.Linq.JObject)")]
111-
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Scope = "member", Target = "Microsoft.Azure.WebJobs.Script.Binding.HttpBinding.#CreateNegotiatedResponse(System.Net.Http.HttpRequestMessage,System.Net.HttpStatusCode,System.Object)")]
111+
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Scope = "member", Target = "Microsoft.Azure.WebJobs.Script.Binding.HttpBinding.#CreateNegotiatedResponse(System.Net.Http.HttpRequestMessage,System.Net.HttpStatusCode,System.Object)")]
112+
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Scope = "member", Target = "Microsoft.Azure.WebJobs.Script.Binding.HttpBinding.#CreateResponse(System.Net.Http.HttpRequestMessage,System.Net.HttpStatusCode,System.Object,Newtonsoft.Json.Linq.JObject,System.Boolean)")]
113+

test/WebJobs.Script.Tests/NodeEndToEndTests.cs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,47 @@ public async Task HttpTrigger_Get()
355355
Assert.Equal(customHeader, reqHeaders["custom-1"]);
356356
}
357357

358+
[Theory]
359+
[InlineData("application/json", "{\"name\": \"test\" }", "rawresponse")]
360+
[InlineData("application/json", 1, "rawresponse")]
361+
[InlineData("application/xml", "<root>XML payload</string>", "rawresponse")]
362+
[InlineData("text/plain", "plain text input", "rawresponse")]
363+
[InlineData("text/plain", "{\"name\": \"test\" }", "rawresponsenocontenttype")]
364+
[InlineData("text/plain", "{\"name\": 1 }", "rawresponsenocontenttype")]
365+
[InlineData("text/plain", "<root>XML payload</string>", "rawresponsenocontenttype")]
366+
[InlineData("text/plain", "plain text input", "rawresponsenocontenttype")]
367+
public async Task HttpTrigger_WithRawResponse_ReturnsContent(string expectedContentType, object body, string scenario)
368+
{
369+
HttpRequestMessage request = new HttpRequestMessage
370+
{
371+
RequestUri = new Uri(string.Format("http://localhost/api/httptrigger-scenarios")),
372+
Method = HttpMethod.Get,
373+
};
374+
request.SetConfiguration(Fixture.RequestConfiguration);
375+
376+
JObject input = new JObject()
377+
{
378+
{ "scenario", scenario },
379+
{ "value", JToken.FromObject(body) },
380+
{ "contenttype", expectedContentType }
381+
};
382+
request.Content = new StringContent(input.ToString());
383+
request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
384+
385+
Dictionary<string, object> arguments = new Dictionary<string, object>
386+
{
387+
{ "req", request }
388+
};
389+
await Fixture.Host.CallAsync("HttpTrigger-scenarios", arguments);
390+
391+
HttpResponseMessage response = (HttpResponseMessage)request.Properties[ScriptConstants.AzureFunctionsHttpResponseKey];
392+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
393+
Assert.Equal(expectedContentType, response.Content.Headers.ContentType.MediaType);
394+
395+
object responseBody = await response.Content.ReadAsStringAsync();
396+
Assert.Equal(body.ToString(), responseBody);
397+
}
398+
358399
[Theory]
359400
[InlineData("application/json", "\"testinput\"")]
360401
[InlineData("application/xml", "<string xmlns=\"http://schemas.microsoft.com/2003/10/Serialization/\">testinput</string>")]

test/WebJobs.Script.Tests/TestScripts/Node/HttpTrigger-Scenarios/index.js

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,27 @@
22

33
module.exports = function (context, req) {
44
var scenario = req.body.scenario;
5-
5+
66
if (scenario == "echo") {
77
context.res = req.body.value;
88
}
9+
else if (scenario == "rawresponse") {
10+
context.res = {
11+
status: 200,
12+
body: req.body.value,
13+
headers: {
14+
'Content-Type': req.body.contenttype
15+
},
16+
isRaw: true
17+
}
18+
}
19+
else if (scenario == "rawresponsenocontenttype") {
20+
context.res = {
21+
status: 200,
22+
body: req.body.value,
23+
isRaw: true
24+
}
25+
}
926
else {
1027
context.res = {
1128
status: 400

test/WebJobs.Script.Tests/TestScripts/Node/functions.tests.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,13 @@ describe('http', () => {
7676
expect(res.get('header')).to.equal('val');
7777
expect(context.isDone).to.be.false;
7878
});
79+
80+
it('raw', () => {
81+
res.raw('test');
82+
expect(res.body).to.equal('test');
83+
expect(res.isRaw).to.be.true;
84+
expect(context.isDone).to.be.true;
85+
});
7986
});
8087

8188
describe('request', () => {

0 commit comments

Comments
 (0)