Today, if you use Invoke-RestMethod and have any sort of malformed data in your request, Invoke-RestMethod eats the server response and instead returns a generic PowerShell error message, greatly increasing pain when troubleshooting.
As an example of desired behavior, it would be good if PowerShell surfaced the remote server response instead of just writing an error and dumping the response.
It would be wonderful if Invoke-RestMethod wrote to an alternate stream or provided rich error data when encountering an error with an endpoint.
For example. note the error data I'm about to get when I used the very useful Failure function provided in this blog post, I'm able to see the full remote server response, which makes troubleshooting this an absolute breeze.
function Failure {
$global:helpme = $body
$global:helpmoref = $moref
$global:result = $_.Exception.Response.GetResponseStream()
$global:reader = New-Object System.IO.StreamReader($global:result)
$global:responseBody = $global:reader.ReadToEnd();
Write-Host -BackgroundColor:Black -ForegroundColor:Red "Status: A system exception was caught."
Write-Host -BackgroundColor:Black -ForegroundColor:Red $global:responsebody
Write-Host -BackgroundColor:Black -ForegroundColor:Red "The request body has been saved to `$global:helpme"
break
}
With this function loaded in memory, I'll run the below code with try/catch and call Failure
on error.
$Splat = @{
Method = 'PUT'
Uri = 'https://api.us.onelogin.com/api/1/users/27697924/add_roles'
ContentType = "application/json"
Headers = @{authorization = "bearer:$token" }
Body = @{role_id_array = (143175)}
}
PS C:\git> Invoke-RestMethod @Splat
Status: A system exception was caught.
{"status":{"error":true,"code":400,"type":"bad request","message":"role_id_array should be -\u003e array of positive integers"}}
The request body has been saved to $global:helpme
In today's PowerShell, all server response data from Invoke-RestMethod
is eaten and a generic PowerShell error is presented. In scouring the results from $error[0]
I was unable to find the actual server response.
$Splat = @{
Method = 'PUT'
Uri = 'https://api.us.onelogin.com/api/1/users/27697924/add_roles'
ContentType = "application/json"
Headers = @{authorization = "bearer:$token" }
Body = @{role_id_array = (143175)}
}
PS C:\git> Invoke-RestMethod @Splat
Invoke-RestMethod : The remote server returned an error: (400) Bad Request.
At line:1 char:1
+ Invoke-RestMethod @Splat
+ ~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (System.Net.HttpWebRequest:HttpWebRequest) [Invoke-RestMethod], WebException
+ FullyQualifiedErrorId : WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeRestMethodCommand
> $PSVersionTable
Name Value
---- -----
PSVersion 5.1.14393.103
PSEdition Desktop
PSCompatibleVersions {1.0, 2.0, 3.0, 4.0...}
BuildVersion 10.0.14393.103
CLRVersion 4.0.30319.42000
WSManStackVersion 3.0
PSRemotingProtocolVersion 2.3
SerializationVersion 1.1.0.1
Excellent write-up Stephen! I can second Stephen's frustration with this issue!
I've definitely been bitten by this.
Same as the above, also using a helper function to get the error response stream. Hope the fix doesn't break those modules though, but even if it does it would be worth it not having to deal with it everytime.
Great write-up!
Essentially, if we just remove one line
response.EnsureSuccessStatusCode();
we will get a pretty decent output.
For example:
Reading web response StatusCode : 400e stream... (Number of bytes read: 229) StatusDescription : BadRequest Content : Error in call to API function "team/get_info": Bad HTTP "Content-Type" header: "application/x-www-form-urlencoded". Expecting one of
"application/json", "application/json; charset=utf-8", "text/plain...
RawContent : HTTP/1.1 400 BadRequest
Server: nginx
Date: Sat, 11 Feb 2017 06:39:47 GMT
Connection: keep-alive
User-Agent:
Error in call to API function "tea...
Forms :
Headers : {[Server, System.String[]], [Date, System.String[]], [Connection, System.String[]], [User-Agent, System.String[]]...}
Images : {}
InputFields : {}
Links : {}
ParsedHtml :
RawContentLength : 229
Btw, in the FullCLR version there is no call to it.
Here's the experience I have coded up, feedback welcome:
PS /home/steve/repos/PowerShell> invoke-webrequest http://httpstat.us/418
invoke-webrequest : 418 I'm a teapot
At line:1 char:1
+ invoke-webrequest http://httpstat.us/418
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (Method: GET, Re...rShell/6.0.
0
}:HttpRequestMessage) [Invoke-WebRequest], I'm a teapot
+ FullyQualifiedErrorId : 418,Microsoft.PowerShell.Commands.InvokeWebReque
stCommand
PS /home/steve/repos/PowerShell> $e.FullyQualifiedErrorId
418,Microsoft.PowerShell.Commands.InvokeWebRequestCommand
The content of the response is stuffed into the Message property of the ErrorDetail and the HTTP Status Code is in the front of the FullyQualifiedErrorId. Response headers are thrown away (not sure if there's any useful case when you receive an error from the server).
HTTP Status Code is in the front of the FullyQualifiedErrorId
Not sure that's a good idea.
@vors, open to suggestions. Could prefix with HttpStatus
, but then it's more parsing for scripts that want the actual code
@PowerShell/powershell-committee (subset of @HemantMahawar @lzybkr @khansen00 and myself) think that the most important piece is that you're able to get at the Response
property of the exception as you can in FullCLR:
When running Invoke-WebRequest http://httpstat.us/418
, the behavior currently differs between Core and Full:
FullCLR:
PS C:\Users\jaiello> $error[1].exception.response
IsMutuallyAuthenticated : False
Cookies : {ARRAffinity=ce89bc5d89e3eae7a38e90a2582b973f890225b3bddc47e95c77a1d7831a8a71}
Headers : {X-AspNetMvc-Version, Access-Control-Allow-Origin, Content-Length, Cache-Control...}
SupportsHeaders : True
ContentLength : 16
ContentEncoding :
ContentType : text/plain; charset=utf-8
CharacterSet : utf-8
Server : Microsoft-IIS/8.0
LastModified : 2/15/2017 4:16:36 PM
StatusCode : 418
StatusDescription : I'm a teapot
ProtocolVersion : 1.1
ResponseUri : http://httpstat.us/418
Method : GET
IsFromCache : False
CoreCLR:
PS C:\Program Files\PowerShell\6.0.0.15> $error[0].exception | fl -force *
Message : Response status code does not indicate success: 418 (I'm a teapot).
Data : {}
InnerException :
TargetSite : System.Net.Http.HttpResponseMessage EnsureSuccessStatusCode()
StackTrace : at System.Net.Http.HttpResponseMessage.EnsureSuccessStatusCode()
at Microsoft.PowerShell.Commands.WebRequestPSCmdlet.ProcessRecord()
HelpLink :
Source : System.Net.Http
HResult : -2146233088
The experience won't be exactly the same as the coreClr types are different, but it's improved and similar:
PS /home/steve/repos/PowerShell> invoke-webrequest http://httpstat.us/418
invoke-webrequest : Response status code does not indicate success: 418 (I'm a teapot).
At line:1 char:1
+ invoke-webrequest http://httpstat.us/418
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (Method: GET, Re...rShell/6.0.0
}:HttpRequestMessage) [Invoke-WebRequest], I'm a teapot
+ FullyQualifiedErrorId : WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeWebRequestCo
mmand
PS /home/steve/repos/PowerShell> $error[0].exception.response
Version : 1.1
Content : System.Net.Http.StreamContent
StatusCode : 418
ReasonPhrase : I'm a teapot
Headers : {[Cache-Control, System.String[]], [Server, System.String[]], [X-AspNetMvc-Version,
System.String[]], [X-AspNet-Version, System.String[]]...}
RequestMessage : Method: GET, RequestUri: 'http://httpstat.us/418', Version: 1.1, Content: <null>,
Headers:
{
User-Agent: Mozilla/5.0
User-Agent: (Windows NT; Linux 4.4.0-64-generic #85-Ubuntu SMP Mon Feb 20 11:50:30
UTC 2017; en-US)
User-Agent: WindowsPowerShell/6.0.0
}
IsSuccessStatusCode : False
Tried another work around to capture error from a REST API is anyone is trying to capture that:
function Failure {
$Message = $_.ErrorDetails.Message;
$json = $Message | ConvertFrom-Json #if error you are trying to capture is returned in json
Write-Host ("Message: "+ $Message)
}
With this function loaded in memory, put your try / catch around Invoke-RestMethod and call Failure on error. Hope this helps.
Most helpful comment
The experience won't be exactly the same as the coreClr types are different, but it's improved and similar: