Tomas Aschan
Tomas Aschan

Reputation: 60584

How do I get the body of a web request that returned 400 Bad Request from Invoke-RestMethod

When I run the following statement

Invoke-RestMethod "https://api.mysite.com/the/endpoint" `
    -Body (ConvertTo-Json $data) `
    -ContentType "application/json" `
    -Headers $DefaultHttpHeaders `
    -Method Post

the endpoint returns 400 Bad Request, which causes PowerShell to show the following not-so-helpful message:

Invoke-WebRequest : The remote server returned an error: (400) Bad Request.
At line:1 char:1
+ Invoke-WebRequest "https://api.mysite.com/the/endpoint" -Body  ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (System.Net.HttpWebRequest:HttpWebRequest) [Invoke-WebRequest], WebException
    + FullyQualifiedErrorId : WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand

How do I get the body of the response, which might tell me what was wrong with the request I sent?

Upvotes: 56

Views: 80545

Answers (9)

Carsten
Carsten

Reputation: 2201

You can work with the HTTPClient to get the raw response:

# initialise the HTTP client:
Add-Type -AssemblyName System.Net.Http
$ignoreCerts = [System.Net.Http.HttpClientHandler]::DangerousAcceptAnyServerCertificateValidator
$handler = [System.Net.Http.HttpClientHandler]::new()
$handler.ServerCertificateCustomValidationCallback = $ignoreCerts
$client = [System.Net.Http.HttpClient]::new($handler)

# define types for sending form-data via POST:
$kvPair = [System.Collections.Generic.KeyValuePair[string,string]]
$kvList = [System.Collections.Generic.List[System.Collections.Generic.KeyValuePair[string,string]]]

$url = "https://api.mysite.com/the/endpoint"

# create the body for the web-call:
$form = $kvList::new()
$form.Add($kvPair::new('grant_type', 'password'))
$form.Add($kvPair::new('username', $user))
$form.Add($kvPair::new('password', $pass))
$content = [System.Net.Http.FormUrlEncodedContent]::new($form)

# send the POST command:
$result = $client.PostAsync($url, $content).Result
$response = $result.Content.ReadAsStringAsync().Result
write-host $response

# close-out:
$client.Dispose()
$handler.Dispose()

Upvotes: 0

Ran Sagy
Ran Sagy

Reputation: 648

While not exactly as the OP intended; I had a similar case with wanting to see the body/headers of the SENT request, rather than the response. The solution is -SessionVariable VarNameHere - You can then inspect this variable and see the exact content of the sent request.

Upvotes: 0

Ivan Akcheurov
Ivan Akcheurov

Reputation: 2373

You can get HTTP response without exceptions regardless if it is 200 or 400:

Powershell 7 introduced -SkipHttpErrorCheck 👏

It works for both Invoke-WebRequest and Invoke-RestMethod:

PS> $res = Invoke-WebRequest -SkipHttpErrorCheck -Method POST https://login.microsoftonline.com/does-not-exist/oauth2/token
PS> $res    
                                                                                                                                                                                                                                                          
StatusCode        : 400                                                                                                                  
StatusDescription : BadRequest                                                                                                           
Content           : {"error":"invalid_request","error_description":"AADSTS900144: The request body must contain the following parameter:
                     'grant_type'.\r\nTrace ID: f40877fd-ae34-4b95-a8d4-c7b8ba613801\r\nCorrelation ID: …
RawContent        : HTTP/1.1 400 BadRequest
                    Cache-Control: no-store, no-cache
                    Pragma: no-cache
                    Strict-Transport-Security: max-age=31536000; includeSubDomains
                    X-Content-Type-Options: nosniff
                    P3P: CP="DSP CUR OTPi IND OTRi…
Headers           : {[Cache-Control, System.String[]], [Pragma, System.String[]], [Strict-Transport-Security, System.String[]], [X-Conte
                    nt-Type-Options, System.String[]]…}
Images            : {}
InputFields       : {}
Links             : {}
RawContentLength  : 503
RelationLink      : {}

MS documentation says:

-SkipHttpErrorCheck


This parameter causes the cmdlet to ignore HTTP error statuses and continue to process responses. The error responses are written to the pipeline just as if they were successful.

Upvotes: 7

Andy Peaple
Andy Peaple

Reputation: 59

The textual response sent from the server is contained in the error variable at the following place:

$_.ErrorDetails.Message

Upvotes: 1

Kai Walter
Kai Walter

Reputation: 4001

For me it only worked in a Pester context, when setting the streams Position to 0 before reading it.

        $statusCode = $null
        $responseBody = $null
        try {
            $response = Invoke-RestMethod -Method GET -Uri "$($apiPrefix)$($operation)" -Headers $headers
            }
        catch [System.Net.WebException] {
            $statusCode = $_.Exception.Response.StatusCode
            $respStream = $_.Exception.Response.GetResponseStream()
            $reader = New-Object System.IO.StreamReader($respStream)
            $reader.BaseStream.Position = 0
            $responseBody = $reader.ReadToEnd() | ConvertFrom-Json
        }
        $statusCode | Should Be $Expected
        $responseBody | Should Not Be $null

Upvotes: 5

Jack Ukleja
Jack Ukleja

Reputation: 13511

If you are just after the response StatusCode and Content here is a novel way of solving this problem without lots of messy try/catch and manual reading of response streams:

# Place the trap within your chosen scope (e.g. function or script)
trap [Net.WebException] { continue; }

# Exceptions are no longer thrown here
$response = Invoke-WebRequest $endpoint

# Check if last command failed
if (!$?)
{   
    # $error[0] now contains the ErrorRecord of the last error (in this case from Invoke-WebRequest)
    # Note: $response should be null at this point

    # Due to the magic of Microsoft.PowerShell.Commands.InvokeWebRequestCommand.WebCmdletWebResponseException
    # we can get the response body directly from the ErrorDetails field
    $body = $error[0].ErrorDetails.Message

    # For compatibility with $response.StatusCode lets cast to int    
    $statusCode = [int] $error[0].Exception.Response.StatusCode
}

As far as I can tell, the ErrorRecord.ErrorDetails.Message contains the exact equivalent to the Microsoft.PowerShell.Commands.WebResponseObject.Content property that would get returned to you on a successful invocation of Invoke-WebRequest, just without the hassle of having to do all that GetResponseStream() jazz.

Upvotes: 4

shyam_
shyam_

Reputation: 2470

$RespErr will have the more details about the BadRequest in my case its

$responce = Invoke-RestMethod -Uri https://localhost:44377/explore/v2/Content -Method Post -Body $PostData -Headers $header -ErrorVariable RespErr;

$RespErr;

{ "error":{ "code":"","message":"The FavoriteName field is required." } }

It looks like it works only in localhost, i tried with my actual server it didn't work.

another way to try is this

    try{
$response = ""
$response = Invoke-WebRequest -Uri https://contentserverint-mhdev.azurewebsites.net/apis/explore/v2/Content?overwrite=true -Method Post -Body $PostData -Headers  $header -ErrorVariable RespErr 
#$response = Invoke-RestMethod -Uri https://localhost:44377/explore/v2/Content?overwrite=true -Method Post -Body $PostData -Headers  $header -ErrorVariable RespErr 
Write-Host "Content created with url="$response.value[0] 

}
catch [System.Net.WebException] {   
        $respStream = $_.Exception.Response.GetResponseStream()
        $reader = New-Object System.IO.StreamReader($respStream)
        $respBody = $reader.ReadToEnd() | ConvertFrom-Json
        $respBody;
 }

Upvotes: 13

brendan62269
brendan62269

Reputation: 1096

There is a known issue with PowerShell Invoke-WebRequest and Invoke-RestMethod where the shell eats the response body when the status code is an error (4xx or 5xx). Sounds like the JSON content you are looking for is evaporating in just this manner. You can fetch the response body in your catch block using $_.Exception.Response.GetResponseStream()

    try {
    Invoke-RestMethod "https://api.mysite.com/the/endpoint" `
        -Body (ConvertTo-Json $data) `
        -ContentType "application/json" `
        -Headers $DefaultHttpHeaders `
        -Method Post
    }
    catch {
        $streamReader = [System.IO.StreamReader]::new($_.Exception.Response.GetResponseStream())
        $ErrResp = $streamReader.ReadToEnd() | ConvertFrom-Json
        $streamReader.Close()
    }

    $ErrResp

Upvotes: 56

AdamL
AdamL

Reputation: 13151

According to Invoke-RestMethod documentation, cmdlet can return different types depending on the content it receives. Assing cmdlet output to a variable ($resp = Invoke-RestMethod (...)) and then check if the type is HtmlWebResponseObject ($resp.gettype()). Then you'll have many properties at your disposal, like BaseResponse, Content and StatusCode.

If $resp is some other type (string, psobject and most probably null in this case), it seems that error message The remote server returned an error: (400) Bad Request is the response body, only stripped from html (I tested this on some of my methods), maybe even truncated . If you want to extract it, run the cmdlet using common parameter to store the error message: Invoke-RestMethod (...) -ErrorVariable RespErr and you'll have it in $RespErr variable.

EDIT:

Ok, I got it and it was pretty obvious :). Invoke-RestMethod throws an error, so lets just catch it:

try{$restp=Invoke-RestMethod (...)} catch {$err=$_.Exception}
$err | Get-Member -MemberType Property

  TypeName: System.Net.WebException

    Name           MemberType Definition
    ----           ---------- ----------
    Message        Property   string Message {get;}
    Response       Property   System.Net.WebResponse Response {get;}
    Status         Property   System.Net.WebExceptionStatus Status {get;}

Here's all you need, especially in WebResponse object. I listed 3 properties that catch the eye, there's more. Also if you store $_ instead of $_.Exception there could be some properties PowerShell already extracted for you, but I don't expect nothing more meaningful than in .Exception.Response.

Upvotes: 18

Related Questions