Skip to content

Push-OutputBinding for HTTP should be more "Powershelly" #208

Open
@JustinGrote

Description

@JustinGrote

I don't think #113 and #93 ever got fully implemented, you still have to provide an "ugly" .NET object to Push-OutputBinding for HTTP Responses.

Instead, the user should be able to pass pretty much any object directly or via the pipeline to Push-OutputBinding and it should "figure it out" the most appropriate way to output it unless the user explicity specifies what they want via optional parameters. This is in line with most powershell command designs.

I made a helper function for Push-OutputBinding that simplifies things for how I think it should work.

using namespace System.Net
using namespace System.IO

function Push-HttpBinding {
<#
.SYNOPSIS
Simplified Output for HTTP Trigger Responses with intelligent defaults
.DESCRIPTION
Push-OutputBinding takes a lot of syntax for a simple response. Since the examples use Response as an HTTP output, we can safely assume that as a default and standard practice, plus there is error checking to warn if this isn't accurate.
We can also assume the user wants to issue an "OK" response unless something otherwise was provided.
We can also "help" users by assuming string arrays and strings were meant as a single item, and detect if some text formats like XML/HTML/JSON were used and set the content type appropriately for easier consumption.
.EXAMPLE
Get-Variable | Push-HTTPBinding
Show all the variables in the current environment. Defaults to JSON output
.EXAMPLE
Get-ChildItem . | ConvertTo-HTML | Push-HTTPBinding
Shows the items in the current folder in HTML table format. Push-HTTPBinding detects XHTML and sets the type accordingly
.EXAMPLE
"<html>test</html>" | Push-HTTPBinding -ContentType "text/plain" -StatusCode Accepted -Header @{"X-MyCoolHeader"="its so cool"}
Outputs html text but overrides the content type to text/plain, statuscode to Accepted, and adds a custom header
#>

    [CmdletBinding()]
    param (
        #The body of the message
        [Parameter(Mandatory,ValueFromPipeline)]$Body,
        #The output binding to send the HTTP response to. Default is Response
        [String]$Name = 'Response',
        #The response code for the message. Default is 200 OK
        [HttpStatusCode]$StatusCode = 'OK',
        #Specify the Content Type of the message. If this is specified, it will be assumed you want the raw object to be output 
        [string]$ContentType,
        #Specify custom headers to include in the HTTP response.
        [HashTable]$Headers
    )

    $Body = $input

    #If a single object in a collection was provided, unwrap that object
    if ($Body.count -eq 1) {$Body = $Body[0]}

    #If a collection of strings was provided, consolidate them to a single string
    if (-not $Body.Where{$PSItem -isnot [String]}) {$Body = $Body -join [Environment]::NewLine}

#region TypeDetection
    #This really should be done in https://github.com/Azure/azure-functions-powershell-worker/blob/9f3179923deb5bb95702da1baaf1dab67fbcea7d/src/Utility/TypeExtensions.cs


    #Passthru if it was any of the already handled types in TypeExtensions.cs
    $typeIsDetected = $false
    ([double],[long],[int],[byte[]],[Stream]).foreach{if ($body -is $psitem) {$typeIsDetected=$true;break}}

    function DetectStringType {
        [CmdletBinding()]
        param (
            [string]$String,
            [regex]$Regex,
            $chars=1000
        )
        #Do a "magic numbers"-style file type detection. Designed to guess file types of large strings to maintain performance at the risk of a potential mismatch
        $buffer = [Char[]]::New($chars)
        ([IO.StringReader]$String).Read($buffer,0,$chars) > $null
        $headerToMatch = $buffer -join ''
        if ($headerToMatch -match $Regex) {$true} else {$false}
    }

    if (-not $ContentType -and -not $typeIsDetected) {

        #XHTML Hint Detection
        #This is primarily assuming someone used ConvertTo-Html to create an HTML document and tried to send it out.
        if (-not $typeIsDetected) {

            if (DetectStringType -String ([String]$Body) -Regex ([Regex]::Escape('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">'))) {
                write-warning "Detected XHTML String, setting content type to application/html+xml"
                $typeIsDetected = $true
                $ContentType = 'application/xhtml+xml'
            }
        }

        #HTML Hint Detection
        if (-not $typeIsDetected) {
            if (DetectStringType [String]$Body -regex '(?s)<html.*?>') {
                write-warning "Detected HTML String, setting content type to text/html"
                $typeIsDetected = $true
                $ContentType = 'text/html'
            }
        }
        

        #XML Hint Detection
        #Try XML. This was tested against large XML and non-XML objects to ensure it both "failed fast and succeeded fast" and didn't try to process the entire file.
        [switch]$isXml = $false

        if ($Body -is [xml.xmlnode]) {
            $isXml = $true
            [String]$Body = $Body.OuterXml
        } else {
            #Very basic XML detection that looks for the first non-whitespace character to be a < then two sets of <> with a little extra criteria, this could certainly be a better regex maybe
            $xmlDetectRegex = '(?s)^\s*?<\w+.+?>.*?<[\w\/]+.+?>.*?'
            if (DetectStringType [String]$Body -regex $xmlDetectRegex) {
                $isXml = $true
            }
        }
        if ($isXml) {
            write-warning "Detected XML String, setting content type to application/xml"
            $typeIsDetected = $true
            $ContentType = 'application/xml'
            [String]$Body = [String]$Body
        }

        #JSON Hint Detection
        if (-not $typeIsDetected) {
            if ($Body -is [newtonsoft.json.linq.jtoken]) {
                $isJson = $true
                [String]$Body = [String]$Body
            } else {
                #Very basic XML detection that looks for the first non-whitespace character to be a < then two sets of <> with a little extra criteria, this could certainly be a better regex maybe
                #Tested against a 180MB json string to make sure it "fails fast"
                $jsonDetectRegex = '(?s)^\s*?[\{\[]+\s*?"\w.+?"\:'
                if (DetectStringType [String]$Body -regex $jsonDetectRegex) {
                    $isJson = $true
                    [String]$Body = [String]$Body
                }
            }
            if ($isJson) {
                write-warning "Detected JSON String, setting content type to application/json"
                $typeIsDetected = $true
                $ContentType = 'application/json'
                [String]$Body = [String]$Body
            }
        }


        #For everything else set the type to application/json because downstream the parser will serialize any remaining objects to json
        if (-not $typeIsDetected) {
            write-warning "Did not detect any types, setting content type to application/json by default"
            $ContentType = 'application/json'
        }
    }

    Push-OutputBinding -Name $Name -Value ([HttpResponseContext]@{
        Body = $Body
        Headers = $Headers
        ContentType = $ContentType
        StatusCode = $StatusCode
    })
}

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions