Private/Functions/Server/Run/Invoke-Route.ps1
function Invoke-Route { <# .SYNOPSIS Parse and invoke a route .DESCRIPTION Given an incoming HTTP request, parse it, select a route and invoke it then return an HTTP response to send back to the client If the route can't be matched, return a 404 response. If an unrecognised exception is caught, return a 500 response If the special TerminateServerException is caught, rethrow to terminate the service cleanly. .PARAMETER Context The incomning HTTP request .OUTPUTS [HttpResponse] - to return to the client. #> param ( [HttpRequest]$Context ) #Write-OperatingSystemLogEntry -EventId ([EventId]::DebugEvent) -Message $Context.RequestHeader() if ($Context.RequestMethod -ieq 'OPTIONS') { # Handle OPTIONS request directly if ($Context.Path -eq '*') { # OPTIONS request at server level return [HttpResponse]::new([HttpStatus]::OK, $Context). AddHeader('Allow', ([HttpRequest]::SupportedRequestMethods -join ', ')). AddHeader('Cache-Control', 'max-age=604800') } # identify request methods for resource $allowedMethods = Get-AllowedRequestMethods -Path $Context.Path if ($null -eq $allowedMethods) { return [HttpResponse]::new([HttpStatus]::NotFound, $Context) } $response = [HttpResponse]::new([HttpStatus]::OK, $Context) if ($Context.IsCorsRequest()) { # CORS preflight - allow anyone. # One day, implement CORS as an attribute on routes and negotiate it properly. # Check allowed methods contains the requested method if ($allowedMethods -inotcontains $Context.Headers['Access-Control-Request-Method']) { return [HttpResponse]::new([HttpStatus]::Forbidden, $Context) } if ($Context.Headers.ContainsKey('Access-Control-Request-Headers')) { $response.AddHeader('Access-Control-Allow-Headers', $Context.Headers['Access-Control-Request-Headers']) | Out-Null } return $response. AddHeader('Access-Control-Allow-Origin', '*'). AddHeader('Access-Control-Allow-Methods', ($allowedMethods -join ', ')). AddHeader('Access-Control-Max-Age', '86400'). AddHeader('Vary', 'Accept-Encoding, Origin') } return $response. AddHeader('Allow', ($allowedMethods -join ', ')). AddHeader('Cache-Control', 'max-age=604800') } # Find route $route = Get-Route -RequestMethod $Context.RequestMethod -Path $Context.Path if (-not $route) { # No match - 404 return [HttpResponse]::new([HttpStatus]::NotFound, $Context) } try { # Invoke the route $result = $route.Invoke($Context.Path) } catch { $addtionalStackTrace = $( if ($_.Exception.InnerException -is [System.Management.Automation.RuntimeException]) { $_.Exception.InnerException.Stacktrace } else { [string]::Empty } ) # Look for an application defined exception $ex = Get-UnderlyingException -Exception $_.Exception # We can't get the full stack trace beneath the invocation error, # but we can get the method that was invoked (see RouteEnrty.Invoke) $invokedMethod = $( try { "`nInvoked method: " + $_.Exception.InvokedMethodSignature } catch { [string]::Empty } ) # Log the exception Write-OperatingSystemLogEntry -EventId ([EventId]::RouteHandlingException) -Message "$($ex.GetType().FullName): $($ex.Message)$($invokedMethod)`n$($addtionalStackTrace + $_.ScriptStackTrace)" if ($ex -is [RestException]) { # Found one - act accordingly if ($ex -is [HttpException]) { return [HttpResponse]::new($ex, $Context) } if ($ex -is [TerminateServerException]) { return [TerminationResponse]::new($Context) } } # Didn't find one - 500 return [HttpResponse]::new([HttpStatus]::InternalServerError, $ex, $Context) } # Based on what was returned by the selected route method, form approptiate response if ($result -is [HttpStatus]) { return [HttpResponse]::new($result, $Context) } if ($result -is [string]) { return [HttpResponse]::new([HttpStatus]::OK, $result, 'text/plain', $Context) } if ($result -is [HttpResponse]) { return $result } if ($result -is [ValueType] -and $result.GetType().IsPrimitive) { # Primitives, e.g. numerics, bools etc. return [HttpResponse]::new([HttpStatus]::OK, $result.ToString(), 'text/plain', $Context) } # Else, some kind of object or a struct like DateTime # # BEWARE: Long lists of complex objects e.g. the result of Get-Process # take many seconds in the ConvertTo-xxx cmdlets! # In cases such as this, you may want to consider returning # reduced information in list-all methods via Select-Object or similar (see tests) foreach($mimeType in $Context.PrioritisedAcceptMimeTypes.Value) { if ('application/json' -like $mimeType) { return [HttpResponse]::new([HttpStatus]::OK, ($result | ConvertTo-Json -Depth 50 -Compress), 'application/json', $Context) } if ('text/xml' -like $mimeType) { return [HttpResponse]::new([HttpStatus]::OK, ($result | ConvertTo-Xml -Depth 50 -As String), 'text/xml', $Context) } if ('text/plain' -like $mimeType) { if ($result -is [PSObject]) { return [HttpResponse]::new([HttpStatus]::OK, ($result | Out-String -Width 5000), 'text/plain', $Context) } else { # If the object here is your own PowerShell class, you should implement ToString() return [HttpResponse]::new([HttpStatus]::OK, $result.ToString(), 'text/plain', $Context) } } } # If we get here, no suitable accept mime type was found return [HttpResponse]::new([HttpStatus]::NotAcceptable, $Context) } |