同事反映 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

Post a comment