Cross edition exception handling with Powershell web cmdlets - powershell

I have an existing PowerShell module that runs against Windows PowerShell 5.1. It depends heavily on the Invoke-WebRequest and Invoke-RestMethod cmdlets which have some fairly significant changes between 5.1 and PowerShell Core 6.
I've already managed to make most of the code work between editions (Desktop/Core), but the last thing I'm having trouble with is handling the exceptions thrown by HTTP failure responses. The existing code looks more or less like this.
try {
$response = Invoke-WebRequest #myparams -EA Stop
# do more stuff with the response
} catch [System.Net.WebException] {
# parse the JSON response body for error details
}
I don't necessarily want to trap any exceptions other than the ones generated by failing HTTP response codes from the server. The exception type returned on Core edition is different than Desktop edition and requires separate code paths to parse the response. So initially I tried this:
try {
$response = Invoke-WebRequest #myparams -EA Stop
# do more stuff with the response
} catch [System.Net.WebException] {
# Desktop: parse the JSON response body for error details
} catch [Microsoft.PowerShell.Commands.HttpResponseException] {
# Core: parse the JSON response body for error details
}
This works fine when running in Core. But when running in Desktop, I get a Unable to find type [Microsoft.PowerShell.Commands.HttpResponseException] error.
What's the best way to work around this? Do I need to just catch all exceptions, string match on the type, and re-throw what I don't want? Is there a more elegant way I'm missing that doesn't involve releasing separate versions of the module for Desktop and Core versions of PowerShell?

I might recommend that you make thin wrapper around the cmdlets that does exception handling, and maybe you create a consistent exception class of your own and each version throws the same one.
At run time, you determine your version/edition, and set an alias to which one you want to use.
Example:
function Invoke-PS51WebRequest {
[CmdletBinding()]
param(...)
try {
Invoke-WebRequest #PSBoundParameters -EA Stop
# return the response
} catch [System.Net.WebException] {
# create a new exception
throw [My.Custom.Exception]$_
}
}
function Invoke-PS6XWebRequest {
[CmdletBinding()]
param(...)
try {
Invoke-WebRequest #PSBoundParameters -EA Stop
# return the response
} catch [System.Net.WebException] {
# Desktop
throw [My.Custom.Exception]$_
} catch [Microsoft.PowerShell.Commands.HttpResponseException] {
# Core
throw [My.Custom.Exception]$_
}
}
switch ($PSVersionTable)
{
{ $_.Is51 } { Set-Alias -Name Invoke-WebRequest -Value Invoke-PS51WebRequest -Force }
{ $_.Is6X } { Set-Alias -Name Invoke-WebRequest -Value Invoke-PS6XWebRequest -Force }
}
try {
Invoke-WebRequest #myParams -ErrorAction Stop
} catch [My.Custom.Exception] {
# do something
}
This needs a lot of work (parsing the exceptions properly, maybe making more than 2 of these variations, determining which platform you're on for real as $PSversionTable.Is51 and .Is6X aren't real, creating your own exception class, creating a proper instance of it instead of casting $_ to it, etc.).
I also demonstrated overriding the actual name Invoke-WebRequest, but I recommend using your own name Invoke-MyCustomWebRequest and using that throughout your code instead. It will keep things more manageable.

Related

foreach continue is not working when inside RunWithElevatedPrivileges

Below code keep executing when error arises,
foreach($url in Get-Content $urlsDir)
{
try
{
// do something
// declare X
}
catch
{
// write host or soemthing with exception
continue
}
finally
{
// dispose X
}
}
but when I put this code in RunWithElevatedPrivileges, it completely stops on first error and won't continue execution,
[Microsoft.SharePoint.SPSecurity]::RunWithElevatedPrivileges({
# Iterate through all webs in a text file
foreach($url in Get-Content $urlsDir)
{
try
{
// do something
// declare X
}
catch
{
// write host or soemthing with exception
continue
}
finally
{
// dispose X
}
}
});
It could be related to whether or not you've specified an ErrorAction, I'm not sure how this relates to try catch though. I have done something similar where my foreach would not stop unless explicitly stated.
Basically you need to specify what should happen if an error occurs on each call, as so
Call-Something $SomeParam -ErrorAction Stop
and unless you specify it as above for each call or in the start of your script as below errors can be silently ignored.
// at the start of your script
$ErrorActionPreference = "Stop"
For more information you can read about ErrorAction in powershell here for instance
https://blogs.msdn.microsoft.com/kebab/2013/06/09/an-introduction-to-error-handling-in-powershell/
And according to msdn these are the valid values:
Stop: Displays the debug message and stops executing. Writes an error to the console.
Inquire: Displays the debug message and asks you whether you want to continue. Note that adding the Debug common parameter to a command--when the command is configured to generate a debugging message--changes the value of the $DebugPreference variable to Inquire.
Continue: Displays the debug message and continues with execution.
SilentlyContinue: No effect. The debug message is not (Default) displayed and execution continues without interruption.

Increasing stack size with WinRM for ScriptMethods

We are currently refactoring our administration scripts.
It had just appeared that a combination of WinRM, error handling and ScriptMethod dramatically decreases available recursion depth.
See the following example:
Invoke-Command -ComputerName . -ScriptBlock {
$object = New-Object psobject
$object | Add-Member ScriptMethod foo {
param($depth)
if ($depth -eq 0) {
throw "error"
}
else {
$this.foo($depth - 1)
}
}
try {
$object.foo(5) # Works fine, the error gets caught
} catch {
Write-Host $_.Exception
}
try {
$object.foo(6) # Failure due to call stack overflow
} catch {
Write-Host $_.Exception
}
}
Just six nested calls are enough to overflow the call stack!
Indeed, more than 200 local nested calls work fine, and without the try-catch the available depth doubles. Regular functions are also not that limited in recursion.
Note: I used recursion only to reproduce the problem, the real code contains many different functions on different objects in different modules. So trivial optimizations as "use functions not ScriptMethod" require architectural changes
Is there a way to increase the available stack size? (I have an administrative account.)
You have two problems that conspire to make this difficult. Neither is most effectively solved by increasing your stack size, if such a thing is possible (I don't know if it is).
First, as you've experienced, remoting adds overhead to calls that reduces the available stack. I don't know why, but it's easily demonstrated that it does. This could be due to the way runspaces are configured, or how the interpreter is invoked, or due to increased bookkeeping -- I don't know the ultimate cause(s).
Second and far more damningly, your method produces a bunch of nested exceptions, rather than just one. This happens because the script method is, in effect, a script block wrapped in another exception handler that rethrows the exception as a MethodInvocationException. As a result, when you call foo(N), a block of nested exception handlers is set up (paraphrased, it's not actually PowerShell code that does this):
try {
try {
...
try {
throw "error"
} catch {
throw [System.Management.Automation.MethodInvocationException]::new(
"Exception calling ""foo"" with ""1"" argument(s): ""$($_.Exception.Message)""",
$_.Exception
)
}
...
} catch {
throw [System.Management.Automation.MethodInvocationException]::new(
"Exception calling ""foo"" with ""1"" argument(s): ""$($_.Exception.Message)""",
$_.Exception
)
}
} catch {
throw [System.Management.Automation.MethodInvocationException]::new(
"Exception calling ""foo"" with ""1"" argument(s): ""$($_.Exception.Message)""",
$_.Exception
)
}
This produces a massive stack trace that eventually overflows all reasonable boundaries. When you use remoting, the problem is exacerbated by the fact that even if the script executes and produces this huge exception, it (and any results the function does produce) can't be successfully remoted -- on my machine, using PowerShell 5, I don't get a stack overflow error but a remoting error when I call foo(10).
The solution here is to avoid this particular deadly combination of recursive script methods and exceptions. Assuming you don't want to get rid of either recursion or exceptions, this is most easily done by wrapping a regular function:
$object = New-Object PSObject
$object | Add-Member ScriptMethod foo {
param($depth)
function foo($depth) {
if ($depth -eq 0) {
throw "error"
}
else {
foo ($depth - 1)
}
}
foo $depth
}
While this produces much more agreeable exceptions, even this can quite quickly run out of stack when you're remoting. On my machine, this works up to foo(200); beyond that I get a call depth overflow. Locally, the limit is far higher, though PowerShell gets unreasonably slow with large arguments.
As a scripting language, PowerShell wasn't exactly designed to handle recursion efficiently. Should you need more than foo(200), my recommendation is to bite the bullet and rewrite the function so it's not recursive. Classes like Stack<T> can help here:
$object = New-Object PSObject
$object | Add-Member ScriptMethod foo {
param($depth)
$stack = New-Object System.Collections.Generic.Stack[int]
$stack.Push($depth)
while ($stack.Count -gt 0) {
$item = $stack.Pop()
if ($item -eq 0) {
throw "error"
} else {
$stack.Push($item - 1)
}
}
}
Obviously foo is trivially tail recursive and this is overkill, but it illustrates the idea. Iterations could push more than one item on the stack.
This not only eliminates any problems with limited stack depth but is a lot faster as well.
Might be worth checking this out if you are overrunning the available memory in your remote session: Running Java remotely using PowerShell
I know it's for running a Java app but the solution updates the max memory available to a remote WinRM session.

Why doesn't Pester catch errors using a trap

I'm wondering why I get the following behaviour when running this script. I have the script loaded in PowerShell ISE (v4 host) and have the Pester module loaded. I run the script by pressing F5.
function Test-Pester {
throw("An error")
}
Describe "what happens when a function throws an error" {
Context "we test with Should Throw" {
It "Throws an error" {
{ Test-Pester } | Should Throw
}
}
Context "we test using a try-catch construct" {
$ErrorSeen = $false
try {
Test-Pester
}
catch {
$ErrorSeen = $true
}
It "is handled by try-catch" {
$ErrorSeen | Should Be $true
}
}
Context "we test using trap" {
trap {
$ErrorSeen = $true
}
$ErrorSeen = $false
Test-Pester
It "is handled by trap" {
$ErrorSeen | Should Be $true
}
}
}
I then get the following output:
Describing what happens when a function throws an error
Context we test with Should Throw
[+] Throws an error 536ms
Context we test using a try-catch construct
[+] is handled by try-catch 246ms
Context we test using trap
An error
At C:\Test-Pester.ps1:2 char:7
+ throw("An error")
+ ~~~~~~~~~~~~~~~~~
+ CategoryInfo : OperationStopped: (An error:String) [], RuntimeException
+ FullyQualifiedErrorId : An error
[-] is handled by trap 702ms
Expected: {True}
But was: {False}
at line: 40 in C:\Test-Pester.ps1
40: $ErrorSeen | Should Be $true
Question
Why is the trap{} apparently not running in the final test?
Here are two solutions to the problem based on the comments/suggested answers from #PetSerAl and #Eris. I have also read and appreciated the answer given to this question:
Why are variable assignments within a Trap block not visible outside it?
Solution 1
Although variables set in the script can be read within the trap, whatever you do within the trap happens to a copy of that variable, i.e. only in the scope that is local to the trap. In this solution we evaluate a reference to $ErrorSeen so that when we set the value of the variable, we are actually setting the value of the variable that exists in the parent scope.
Adding a continue to the trap suppresses the ErrorRecord details, cleaning up the output of the test.
Describe "what happens when a function throws an error" {
Context "we test using trap" {
$ErrorSeen = $false
trap {
Write-Warning "Error trapped"
([Ref]$ErrorSeen).Value = $true
continue
}
Test-Pester
It "is handled by trap" {
$ErrorSeen | Should Be $true
}
}
}
Solution 2
Along the same lines as the first solution, the problem can be solved by explicitly setting the scope of the $ErrorSeen variable (1) when it is first created and (2) when used within the trap {}. I have used the Script scope here, but Global also seems to work.
Same principle applies here: we need to avoid the problem where changes to the variable within the trap only happen to a local copy of the variable.
Describe "what happens when a function throws an error" {
Context "we test using trap" {
$Script:ErrorSeen = $false
trap {
Write-Warning "Error trapped"
$Script:ErrorSeen = $true
continue
}
Test-Pester
It "is handled by trap" {
$ErrorSeen | Should Be $true
}
}
}
According to this blog, you need to tell your trap to do something to the control flow:
The [...] thing you notice is that when you run this as script, you will receive both your error message and the red PowerShell error message.
. 'C:\Scripts\test.ps1'
Something terrible happened!
Attempted to divide by zero.
At C:\Scripts\test.ps1:2 Char:3
+ 1/ <<<< null
This is because your Trap did not really handle the exception. To handle an exception, you need to add the "Continue" statement to your trap:
trap { 'Something terrible happened!'; continue }
1/$null
Now, the trap works as expected. It does whatever you specified in the trap script block, and PowerShell does not get to see the exception anymore. You no longer get the red error message.

Switching on SqlException.Number in Powershell

I have this catch block in my Powershell script.
catch [System.Data.SqlClient.SqlException]
{
Write-Host "$_"
Exit 2
}
I would really like to be able to switch on the error number.
I know atleast in C# there's a property on the SqlException called number. Isn't that also true for Powershell?
If the property is there, how do I access it?
Thank you very much in advance
You should be able to access it in your catch block using:
$_.Exception.Number
i.e.
catch [System.Data.SqlClient.SqlException]
{
Write-Host $_.Exception.Number
Exit 2
}

Detecting a .NET exception in PS

I am making several calls to .NET classes in order in my PS script like this:
[class1]::MethodA()
[class1]::MethodB()
if(/*check if last method threw an error*/)
{
"MethodB failed! Exiting."
return
}
[class2]::MethodC()
[class2]::MethodD()
I really want to check whether there was an exception from MethodB before moving on. How do check for this?
Use a try/catch block:
try
{
[class1]::MethodB()
}
catch
{
Write-Host "MethodB failed!"
exit
}