-
Notifications
You must be signed in to change notification settings - Fork 448
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Added expressjs style api for http responses #781
Conversation
} | ||
} | ||
|
||
if (jo.TryGetValue("statusCode", StringComparison.OrdinalIgnoreCase, out value) && value is JValue) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
needed to add statusCode
so that the response object could use the express status(...)
function
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you merge this if with the previous one?
function attachResponseObjects(context) { | ||
if (context._httpOutputBindings) { | ||
var bindings = context._httpOutputBindings; | ||
// if there is only one output http binding use the context.res shortcut |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We really only support a single http output binding right now. Not sure if we actually prevent multiple (we should probably prevent this) but multiple won't actually make sense, since there can only be one response. If a user added multiple, our current behavior would be unpredictable. Let's make this a singleton.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yep makes sense, for some reason had it in my head that http output bindings could act as a httpclient and fire off new requests
var bindings = context._httpOutputBindings; | ||
// if there is only one output http binding use the context.res shortcut | ||
if (bindings.length === 1) { | ||
context['res'] = res(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can't this be context.res
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it is context.res, yep
.Select((FunctionBinding binding) => binding.Metadata.Name) | ||
.ToArray(); | ||
|
||
context["_httpOutputBindings"] = httpOutputBindings; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we name this _httpResponseName?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
changed so it's a simple boolean _http
@@ -371,6 +371,12 @@ protected override void OnScriptFileChanged(object sender, FileSystemEventArgs e | |||
|
|||
bindings.Add(_trigger.Name, input); | |||
|
|||
var httpOutputBindings = _outputBindings.Where((FunctionBinding binding) => binding.Metadata.Type == "http") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- Since we should just make this a .First(...) since we only really support one currently
- make the string comparison OrdinalIgnoreCase
- nit: use Linq short form .Where(p => p.Metadata.Type) ...
@@ -69,6 +70,11 @@ function createFunction(f) { | |||
inputs.unshift(context); | |||
delete context._inputs; | |||
|
|||
if (context._http) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
perhaps generalize this to context._triggerType so it can be used for other things in the future? So the invoker adds the trigger type, and we remove the property after use, before passing context to script.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sounds reasonable. I was originally thinking more in terms of output bindings, but as there's 1:1 mapping for httptrigger -> httpoutputbinding this work
@@ -371,6 +371,11 @@ protected override void OnScriptFileChanged(object sender, FileSystemEventArgs e | |||
|
|||
bindings.Add(_trigger.Name, input); | |||
|
|||
if (_outputBindings.Any((FunctionBinding binding) => binding.Metadata.Type == "http")) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: _outputBindings.Any(p => string.Compare(binding.Metadata.Type, "http", StringComparison.OrdinalIgnoreCase) == 0)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not nit: _outputBindings.Any(p => string.Compare(binding.Metadata.Type, "http", StringComparison.OrdinalIgnoreCase) == 0)
:)
module.exports = function (context, req) { | ||
context.log('Node.js HttpTrigger function invoked.'); | ||
|
||
context.res.status(200) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about a corresponding programming model for request (req)? E.g. Express has a bunch of interesting methods that are applicable like req.accepts(), req.get(), req.params(), etc. I think if we're adding a model for response, we should for requests as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah, I'll expand this, accepts and get are definitely useful. Correct me if I'm wrong, but we don't support params for httptriggers (params like functionurl/:id
, not query params)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes we do: see #743
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
slick, I'll add
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm already adding captured route params to req.params
here
// Licensed under the MIT License. See License.txt in the project root for license information. | ||
|
||
module.exports = () => { | ||
var res = {}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we rename the file/module to something more descriptive (e.g. response
, httpresponse
, funchttpresponse
, etc.)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pretty standard practice in express: http://expressjs.com/en/api.html#res, but not opposed to changing to response
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mathewc brought that up as well so I think we're on the same page and could move forward with the rename, unless if you feel strongly that it should remain res
.
@@ -371,6 +371,11 @@ protected override void OnScriptFileChanged(object sender, FileSystemEventArgs e | |||
|
|||
bindings.Add(_trigger.Name, input); | |||
|
|||
if (_outputBindings.Any((FunctionBinding binding) => binding.Metadata.Type == "http")) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not nit: _outputBindings.Any(p => string.Compare(binding.Metadata.Type, "http", StringComparison.OrdinalIgnoreCase) == 0)
:)
return res; | ||
}; | ||
|
||
res.send = res.json = (body) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need res.end()? Not sure if this is something people expect.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, doesn't res.json have behavior (e.g. set application/json content type)? Otherwise, why have two different methods?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So - with single http output res.end is great, because now I can automatically wire up context.done to it.
Debated on what to do for res.json as there's a bunch of logic for processing body type in HttpBinding.cs. To avoid confusion & try to express intent I should set content-type to application/json
anyway and then the c# webapi stuff can do its content negotiation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Express sets text/plain for strings, application/octet-stream for buffer, etc. We should just make sure that these simple helpful behaviors are there. Might be simpler to add these to this node module, as long as our existing logic in httpbinding won't conflict. Also need to make sure overrides work as expected - e.g. if they called set to set content type explicitly, don't set it, etc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mamaso , I might be missing your point, but I'm confused about this statement:
... I should set content-type to
application/json
anyway and then the c# webapi stuff can do its content negotiation.
We need to set the content type to application/json
if that content type is what is expected when using express and res.json
, and in that case, there would be no conneg. The runtime would honor the content-type
set in the function and that would be used no matter what the Accept
header stated.
If, on the other hand, we do not set the content-type
header when using res.json
(and the others mentioned by Mathew above), then we would end up relying on conneg and the result could be unexpected if the intention is to match the express behavior (as what the client specifies in the accept
header would be evaluated and you could end up with a different content-type
)
This is unrelated to your changes, but before commenting I ran a few tests with our current logic that would trigger content negotiation in Node and we have some issues we need to address. We can chat with the larger group tomorrow and open issues to document them.
}; | ||
|
||
res.set = res.header = (field, val) => { | ||
if (!res.headers) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: missing braces
@@ -69,6 +70,11 @@ function createFunction(f) { | |||
inputs.unshift(context); | |||
delete context._inputs; | |||
|
|||
if (context._http) { | |||
context.res = res(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Take a look at the crappy logic we have here: https://github.com/Azure/azure-webjobs-sdk-script/blob/dev/src/WebJobs.Script/Binding/HttpBinding.cs#L60 where we're trying to sniff whether the returned object is a json response object v.s. a user defined object. The flaw with the existing logic is that it hinges on a single 'body' property. However, that is faulty if someone tries to return a custom poco with a body property that ISN'T intended to be an http response.
Can your new type help here? E.g. if your context.res module object had a private field that we checked for here, we'd know the intent was for this to be a response object.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ah, so at present to get around that you'd need to nest your poco like...
body: {
//poco stuff
body: 'poco property'
}
I'll think about it, certainly could improve the logic a bit
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here's the question: do we want to make this a breaking change and force everyone to migrate to using the express-like api & response objects?
Otherwise we'll still need to assume that a returned object could be poco or a response object via the above logic, I don't see a way around it.
context.res.status(200) | ||
.set('test-header', 'Test Response Header') | ||
.set('Content-Type', 'application/json; charset=utf-8') | ||
.send({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In Express, send will actually send the response, right? Whereas here, this is just a builder, and we don't actually send the response until much later, after context.done() is called, and all output bindings are processed?
Should we tie send/end to context.done?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that tying send & end to context.done is the right move, it will match the express model. Potential negative is if there are multiple output bindings, could get a little confusing.
Should Buffer be supported as a body object? With Express documentation:
|
Yep @yvele, buffer should be supported (and is pretty straightforward https://github.com/expressjs/express/blob/master/lib/response.js#L147) Edit: unfortunately, not so straightforward due to our node -> c# marshaling. I think I'll push buffers to a later PR |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's get this in for 0.8 as some of the other changes discussed are a bit out of scope for this PR.
Just to let you know that I've open sourced my Azure Function to Express adapter: https://github.com/yvele/azure-function-express 😆 |
resolves #160
Went with a very basic wrapper, can add functionality as necessary but these basic functions cover most use cases.
If there is only one output http binding, we populate context.res, otherwise we populate context.bindings.
Not a breaking change as these response objects can be overwritten with
{ headers: ..., status: ..., body: ... }
objects