10

I wrote a small PowerShell script to send a request to a server and get a simple XML result back.

PowerShell Script

$xml = "<?xml version='1.0' encoding='utf-8'?><Search><ID>278A87E1-1BC2-4E19-82E9-8BBE31D67D20</ID></Search>"
$response = Invoke-RestMethod -Method Post -Uri "http://localhost/search" -ContentType "application/xml" -Body $xml

That's it, really simple and there's no reason that I can see for it to be failing. I have also tried the script with Invoke-WebRequest and both fail. The error returned is Invoke-RestMethod : Value cannot be null. Parameter name: name. The strange thing is that when I monitor this with Wireshark, I see the connection, I see the POST and I see the result from the server and all looks perfectly good but the cmdlet is saying it failed (and yes, the return code is 200).

If I run the Invoke-WebRequest/Invoke-RestMethod with the -OutFile parameter, it runs fine with no errors and saves the result to the specified file; -OutVariable fails just in case you're wondering.

The result is an xml file, the headers specify that it is xml and the xml is properly formatted.

Result when successful

<?xml version="1.0" encoding="UTF-8" ?>
<Result version="1.0" xmlns="urn:xmlns-org">
    <searchID>{278a87e1-1bc2-4e19-82e9-8bbe31d67d20}</searchID>
    <responseStatus>true</responseStatus>
    <responseStatusStrg>MORE</responseStatusStrg>
    <numOfMatches>40</numOfMatches>
</Result>

Does anyone know why the Invoke-XXX cmdlets are returning an error and what I can do to fix it? Again, it works perfectly fine when I use the -OutFile parameter and even when it fails I can see a proper conversation between the script and the server in Wireshark.

Also, if I use -Verbose it tells me the following:

VERBOSE: POST http://localhost/search with -1-byte payload
VERBOSE: received X-byte response of content type application/xml; charset="UTF-8"

Where X-byte is the actual size of the response but it obviously differs with each response depending on the data sent to the server. I just find it odd that the cmdlet fails but says it received a response with data and that it sent a -1-byte payload.

vane
  • 2,125
  • 1
  • 21
  • 40
  • So 4 years later, did you happen to come up with a solution to this problem? I am running into the exact same thing. Right now my "solution" looks like it's going to have to be migrating to a Win2016 server, where this problem doesn't seem to occur. –  Aug 06 '20 at 16:19
  • Unfortunately no. The solution is either to patch the assembly with the buggy code, make the server you're requesting from return a value that Invoke-WebRequest can handle (if you own the server), or like you said change to a different version of Windows that doesn't have that issue. Although, it might be worth a try to just install the win2016 version of powershell on whatever version of Windows you're using now but I haven't tested that so your mileage may vary – vane Aug 07 '20 at 17:18

2 Answers2

13

I went ahead and looked into the Invoke-WebRequest cmdlet code and found out why it's failing with this particular error.

It's failing on a call to System.Globalization.EncodingTable.GetCodePageFromName. The encoding is passed to that function as a parameter and the encoding is retrieved from the the cmdlet through the Content-Type header. In the case of this server the Content-Type was sent back in the response as Content-Type: application/xml; charset="UTF-8".

The problem with this is that quotes aren't standard for wrapping the value in charset so the cmdlet parses it out as "UTF-8" instead of the valid UTF-8. The cmdlet passes "UTF-8" to the function and the function throws an exception stating that the provided encoding is invalid. This is fine and would make so much more sense if that is what was reported in the final exception but it's not.

The Invalid encoding exception is caught by the Microsoft.PowerShell.Commands.ContentHelper.GetEncodingOrDefault function and in the exception handler it calls GetEncoding again but with a null parameter which results in the final ArgumentNullException for parameter name.

Microsoft.PowerShell.Commands.ContentHelper.GetEncodingOrDefault

internal static Encoding GetEncodingOrDefault(string characterSet)
{
  string name = string.IsNullOrEmpty(characterSet) ? "ISO-8859-1" : characterSet;
  try
  {
    return Encoding.GetEncoding(name);
  }
  catch (ArgumentException ex)
  {
    return Encoding.GetEncoding((string) null);
  }
}

The call to GetEncoding inside the catch statement triggers the following code inside GetCodePageFromName which is itself called from GetEncoding

if (name==null) { 
    throw new ArgumentNullException("name");
}

PowerShell is handling this properly since technically it is an invalid value but you'd think they would call Trim("\"") just to be safe.

vane
  • 2,125
  • 1
  • 21
  • 40
  • I'm running into this problem with Python 2.7's `SimpleHTTPServer`. – Alyssa Haroldsen Jan 22 '17 at 07:34
  • 1
    Thanks for discovering the root cause. I've seen this manifest on a fully patched Windows Server 2012 R2 box with PowerShell (WMF) 5.1 installed. Take the same code over to Windows 10 or Windows Server 2016 and there is no issue. Very disappointing as there doesn't seem to be a solution. – Lewis Jun 29 '17 at 13:13
0

Although this thread is years-old, I came across it in 2022 when I encountered the same error 'Invoke-RestMethod : Value cannot be null. Parameter name: name' accessing an API to create a user. In my case Content-Type was application/json (rather than application/xml in Vane's question above). The API POST required Basic Auth.

I UTF8 encoded the credentials prior to converting to Base64, and et voilà the error disappeared. Given that the Content-Type investigation by Vane above tracked a return of charset="UTF-8", this UTF8 encoding and the disappearance of the error may be related, although this is just surmise on my part.

$username = "your_username"
$password = "your_password"
$credential = "${username}:${password}"
$credentialBytes = [System.Text.Encoding]::UTF8.GetBytes($credential)
$encodedCredential = [System.Convert]::ToBase64String($credentialBytes)
$headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]"
$headers.Add("Authorization", "Basic $encodedCredential")
$headers.Add("Content-Type", "application/json")