【茶包射手日記】PowerShell Invoke-WebRequest 呼叫 IIS 出錯時顯示亂碼
0 |
同事反映 WebAPI 有問題,我提供了一段 PowerShell Invoke-WebRequest -Method POST 程式片段請他對照錯誤,他回報出現 502 錯誤,但訊息是亂碼:(為方便觀察及說明,以下用 HTTP GET 500 錯誤代替)
觀察錯誤回傳結果,找到原因:IIS 所在作業系統是中文版 Windows,出錯時吐回的自訂錯誤畫面,採用的還是古老的 charset=big5 編碼。
由此推測 PowerShell 在出錯時,試圖解析 HTTP 錯誤網頁的 HTML 將其轉換為純文字,但錯在未能正確識別成 BIG5 編碼,錯用 UTF-8 解析,才噴出亂碼。用一小段 C# 模擬用 UTF-8 硬解 BIG5 編碼的「伺服器錯誤 500 - 內部伺服器錯誤。 您要尋找的資源有問題而無法顯示。」字串,得到完全相同的結果,可得證。
猜想應是網站吐回的內容未指定 CharSet,PowerShell 應會採用預設編碼解析,我如果能將預設編碼暫時改為 Big5,問題就有解了。找不到有文件提及如何修改 Invoke-WebRequest 解析 HTML 時的預設語系編碼,決定從原始碼找答案。我查到當 HTTP 狀態碼失敗時的解析邏輯,是透過 WebResponseHelper.GetCharacterSet(response) 取得 CharSet,作為後續 StreamHelper.DecodeStream 時的參數。
if (ShouldCheckHttpStatus && !_isSuccess)
{
string message = string.Format(
CultureInfo.CurrentCulture,
WebCmdletStrings.ResponseStatusCodeFailure,
(int)response.StatusCode,
response.ReasonPhrase);
HttpResponseException httpEx = new(message, response);
ErrorRecord er = new(httpEx, "WebCmdletWebResponseException", ErrorCategory.InvalidOperation, request);
string detailMsg = string.Empty;
try
{
// We can't use ReadAsStringAsync because it doesn't have per read timeouts
TimeSpan perReadTimeout = ConvertTimeoutSecondsToTimeSpan(OperationTimeoutSeconds);
string characterSet = WebResponseHelper.GetCharacterSet(response);
var responseStream = StreamHelper.GetResponseStream(response, _cancelToken.Token);
int initialCapacity = (int)Math.Min(contentLength ?? StreamHelper.DefaultReadBuffer, StreamHelper.DefaultReadBuffer);
var bufferedStream = new WebResponseContentMemoryStream(responseStream, initialCapacity, this, contentLength, perReadTimeout, _cancelToken.Token);
string error = StreamHelper.DecodeStream(bufferedStream, characterSet, out Encoding encoding, perReadTimeout, _cancelToken.Token);
detailMsg = FormatErrorMessage(error, contentType);
}
catch (Exception ex)
{
// Catch all
er.ErrorDetails = new ErrorDetails(ex.ToString());
}
if (!string.IsNullOrEmpty(detailMsg))
{
er.ErrorDetails = new ErrorDetails(detailMsg);
}
ThrowTerminatingError(er);
}
追進去,WebResponseHelper.GetCharsetSet()由 response.Content.Headers.ContentType?.CharSet
取得 CharSet (可能是 null),
檢查 IIS 出錯時回傳自訂錯誤頁面的 Response Header,Content-Type 只有 text/html 無 CharSet,故 PowerShell 會抓到 null。
當 CharSet 作為參數傳入 StreamHelper.DecodeStream() 解析 HttpResponseStream,CharSet 缺值或不對時會改用 ContentHelper.GetDefaultEncoding() 為準,該方法寫死 internal static Encoding GetDefaultEncoding() => Encoding.UTF8;
沒得改,宣告此路不通。
題外話,Github 現在認得 C#,能簡單識別類別、方法,列出有參考到它的地方,還蠻好用的。
出不轉路轉,如果你有 Cmder、Git Bash 或 Windows 10/11 (見文末更新),可以改用 curl 存成 HTML 再看:
如果吞不下這口氣,或測試機器上沒有 curl 等工具,一定要用 PowerShell 讀出結果,可以 catch WebException 再用 StreamReader 指定 BIG5 編碼開啟 ResonseStream,即能正確讀出內容。
try {
Invoke-WebRequest -Uri http://192.168.50.3
}
catch [System.Net.WebException] {
$stm = $_.Exception.Response.GetResponseStream()
$stm.Position = 0
$big5Text = (New-Object System.IO.StreamReader($stm, [System.Text.Encoding]::GetEncoding(950))).ReadToEnd()
$html = New-Object -Com 'HTMLFile'
$html.IHTMLDocument2_write($big5Text)
$html.body.InnerText.Split([System.Environment]::NewLine, [System.StringSplitOptions]::RemoveEmptyEntries)
}
2023-07-21 更新
2017/11/19 起 Windows 10/11 已經內建 curl 了 (Windows Server 沒有) 參考,因為在 PowerShell curl 被導向 Invoke-WebRequest,我一直沒發現。感謝讀者 Kuo Chin-Yuan 補充。
Research of why PowerShell Invoke-WebRequest can't read the IIS error page with correct encoding.
Comments
Be the first to post a comment