In a small project, I was trying to utilize an existing PowerShell I had, and host it in Azure Functions; I needed to understand how HTTP binding work with PowerShell Azure Functions as I didn’t want to rewrite my script to C# just because the PowerShell Azure Functions had the “(Preview)” appended to its name.
I wanted the Function to return a plain text response to an HTTP trigger based on a query parameter (this is how Dropbox verifies Webhook URLs). So, naively, I followed the basic template as an example:
Write-Output "PowerShell HTTP function invoked" if ($req_query_name) { $message = "$req_query_name" } else { $message = "wrong!" } [io.file]::WriteAllText($res, $message)
The first question I had was “how is the querystring parsed?” I assumed that I should replace “req_query_name” with the querystring key in the request, should I replace the whole thing to become $myQueryParam? This is when I decided to look in the source code rather than the documentation.
Note: I try to link back to the source code wherever I can, the problem is the link does not include the commit ID, so next to the link I put the commit ID at which the file was in that state.
HTTP Binding
There are different phases that take place during a Function execution, in this post I will skip the details of how the binding is loaded, and only concentrate on how the HTTP binding operates within a PowerShell Function.
Input
When the Azure Functions runtime receives an HTTP message for PowerShell script that has HTTP binding, it parses the message according to the following:
- The body of the HTTP request will be saved to a temp file, the path of the temp file will be assigned to an environment variable that matches the “Name” property of the input binding configuration. If we take the following JSON as an example for our “function.json” configuration, then the name of the variable will be “req“:
{ "bindings": [ { "name": "req", "type": "httpTrigger", "direction": "in", "authLevel": "function" }, { "name": "res", "type": "http", "direction": "out" } ], "disabled": false }
- The original URL will be saved in environment variable “REQ_ORIGINAL_URL“.
- The HTTP request method will be saved in environment variable “REQ_METHOD“.
- For each HTTP header “key”, a corresponding environment variable “REQ_HEADERS_key” will be created
- The full querystring will be saved in environment variable “REQ_QUERY“, it will also be further parsed into individual variables; for each query string “key”, a corresponding variable “REQ_QUERY_key” will be created.
All of this happen before the execution of the Function, so once the Function is invoked these variables are already available for consumption. (This happens here at dcc9e1d ).
To read the body of the request you just read it as you read any file PowerShell, and then you parse it according to the content; so if the body of the request is JSON you read the file and parse it to JSON like the following:
$mycontent = Get-Content $req | ConvertFrom-Json
Note: If the Function is executing because of a Triggered bindings (such as HTTP), the rest of the input bindings are skipped. (Check the code here at commit dcc9e1d)
Output
Similar to the request, your script should write the response to a file, which in turn will be read by the Azure Functions runtime, and then will pass it to the HTTP output binding to send it on your behalf . The runtime will also assign the path of this file to an environment variable that matches the Name property you define in the output binding in the function.json.
So for the example above of function.json, you will write the content of your response to the file whose path is stored in “res”:
[io.file]::WriteAllText($res, $message)
This happens here at commit dcc9e1d.
Default Behaviour
Now, if the content you write to the file is a string that cannot be parsed to JSON, then: it will be considered as the body of the HtttpMessage, the response will have the default HTTP content-type “application/json”, and it will be run through the default MediaTypeFormatter. Take the following as an example:
Function:
$message = "This is a text" [System.IO.File]::WriteAllText($res,$message)
Result:
Content-Type: application\json "this is a text"
Notice that the text written to the file in the script is without quotes, but the result in the response body is in double quotes; this is because the default content-type of the response is “application/json”, and the HTTP binding will format it accordingly and wrapp in double quotes.
More Control
If we want more control over the response then you have to write JSON object to the file, this JSON object will hold all the information on how the response should look like: the headers, the body, and the response status.
The JSON object should contain the properties: “body“, “headers“, “isRaw” (more about it below), and “statusCode” (int) if you want to change any. For example, if I want the content of the response to be simple text with plain/text content-type , then the script should write the following:
$message = "{ `"headers`":{`"content-type`":`"text/plain`"}, `"body`":`"$name`"}" [System.IO.File]::WriteAllText($res,$message)
There are several points that need to be brought up:
- If the “body” property exists, then only the value of the “body” property will be in the HttpMessage body, otherwise the whole content of the JSON object will be in the HttpMessage body.
- Up until the time of writing this post, Azure PowerShell functions runs under PowerShell 4.0, this means that if you want to use the Out-File command to write to the file, then it will always append a new line feed (\r\n) at the end of the string, even if you supply the -NoNewLine parameter! Use the WriteAllText command instead.
The parsing can be found here at commit 3b3e8cb.
Formatters
Great, so far we managed to change the body, the headers (including the content-type), and the status of the response. But this is also not enough; depending on the content-type header, the Azure Functions runtime will find the right MediaFormatter for the content and format the response body with the right format.
There are several types of MediaFormatters in the System.Net.Http.Formatting library: JsonMediaTypeFormatter, FormUrlMediaFormatter, XmlMediaTypeFormatter, and others. The issue with the formatters is that it might add the UTF-8 Byte Order Mark (BOM) at the beginning of the content. If the recipient is not ready for this it might cause a problem.
Dropbox, for example, provides a way to watch the changes to a file through their API by registering a webhook, and the way Dropbox verifies the webhook is by making a request to the endpoint with a specific querystring, then it expects the webhook to respond by echoing the querystring back. When I created my Function I didn’t change anything, thus the runtime used the default formatter and appended the UTF-8 BOM characters (0xEF,0xBB,0xBF
) to the beginning of the body, which of course was revoked by Dropbox.
The way to skip these formatters is by setting the “isRaw” property mentioned above to true. For example, the following script will write a plain text “emad1234” to the response:
$message = "{ `"headers`":{`"content-type`":`"text/plain`"}, `"body`":`"emad1234`" }"
Taking a screenshot from Fiddler from the HexView view, the response look like this:
Have you noticed the characters I surrounded with the red box? that’s the BOM, displayed as “
“.
But once we add the “isRaw” property like this:
$message = "{ `"isRaw`": true, `"headers`":{`"content-type`":`"text/plain`"}, `"body`":`"emad1234`" }"
The result will be without the BOM:
This can be found here at commit 3b3e8cb.
Final Notes
It’s worth mentioning that Azure Functions runtime also provides content-negotiation feature, and you can leave it to the request to decide.
Another departing thought is that of course you don’t have to craft your JSON object by concatenating strings together, you can use PowerShell arrays and hashtables to do that, check the articles here and here.
Finally, isn’t it awesome to be able to see that in the source code!
Conclusion
PowerShell probably is the language that got the least love from the Azure Functions team, but this does not mean that you throw your scripts away, hopefully with the tips in this post you will find a way to use them again.
Great Article. thank you!
Up until the time of writing this post, Azure PowerShell functions runs under PowerShell 4.0, this means that if you want to use the Out-File command to write to the file, then it will always append a new line feed (\r\n) at the end of the string, even if you supply the -NoNewLine parameter!
This has been fixed, adding -NoNewLine now does NOT add a new line feed (\r\n) at the end of the string
Do you know if its possible to get the request header Content-Type value using the REQ_HEADERS_ format