2019/01/28

WebBroker and Unicode


以下的內容在 Delphi 10.2 (含)以上版本已經解決,新版使用者可以略過不看。

===========================================

TWebRequest 遇到 Unicode

出來江湖混,該還的還是要還。

前端寫久了,還是遇到了 Unicode 解析問題。

以 HTTP Method GET 為例:
http://{local}/action?id=許功蓋堃

已知:
Delphi 2009 以上的 string = UTF-16 格式。

而取得的內容是這樣的:

看到這個心都涼了半截

仔細攻略了 IdHTTPWebBrokerBridge 和 HTTPApp 兩個單元後,才發現裡頭大部份的函式及屬性都是 AnsiString 而非 String 基礎類別。

而 IdHTTPWebBrokerBridge 單元主要只應用在 HTTP Server 上,搬上 IIS 後就和它無關,所以可以直接忽略這個單元不看,雖然我也是看了它很久很久才驚覺這個答案。(眼神死)


接著再把注意力轉到 HTTPApp 單元,最關鍵的就在這句話:
一律使用 AnsiString,Unicode 都不 Unicode 了

ExtractHeaderFields 這個函式遇到要解析的內容僅僅只做了百分比符號轉換,還少做了一道Unicode / UTF-8 (UTF8) 編碼轉換的工作。

於是我們的 Unicode 文字就被轉成亂碼直出了。

現在,到底是該 Debug HTTPApp 還是有其它 3rd party 的解法呢?



HTTPApp.pas 的低耦合性,適合提出來獨立處理。
來看看這段可以怎麼改:

Strings.Add(UTF8ToString(HTTPDecode(AnsiString(DoStripQuotes(ExtractedField)))))
or
Strings.Add(IdURI.TIdURI.URLDecode(AnsiString(DoStripQuotes(ExtractedField))))


都可以解決 TWebRequest 屬性 QueryFields/ContentFields 取得資料的亂碼問題。







你以為以上就結束了嗎?別忘了還有 TWebResponse 呢!

============== 2021/10/05 更新 ==============

在 uses 裡引用 UTF8ContentParser 此單元,上述問題都迎刃而解,這單元真的很少人提過,以下是來自檔案中的說明內容:

TUTF8ContentParser is a WebRequest content parser that parses UTF-8 requests.
TUTF8ContentParser class automatically replace the default content parser when this unit (UTF8ContentParser)
is used in a web application.  You should only use UTF8ContentParser in web applications that generate UTF-8
responses.

To generated UTF-8 encoded responses, set Response.ContentType as follows before setting Response.Content.
   Response.ContentType := 'text/html; charset=UTF-8';

Note that, if your application uses the ReqMulti unit to parse multipart content, ReqMulti must appear in the application
uses list after UTF8ContentParser.
文件中顯示 TWebResponse 也能一併解決,但測試發佈【不帶 BOM 的 UTF-8 檔案】時仍有亂碼出現。主要還是舊版 WebBroker 預設會以 windows-1252 編碼輸出,另一個原因是 Ansistring 沒有設定 codepage 的關係。

TWebResponse 遇到 Unicode


TWebResponse.Content 雖然是 string 類別,但裡面是這樣寫的:

procedure TWebResponse.SetUnicodeContent(const AValue: string);
var
  AAnsiString: AnsiString;
  Bytes: TBytes;
begin
  Bytes := EncodingGetBytes(ContentType, AValue);

  SetLength(AAnsiString, Length(Bytes));
  if Length(AAnsiString) > 0 then
    SetString(AAnsiString, PAnsiChar(@Bytes[0]), Length(Bytes));
  SetContent(AAnsiString);
end;

這裡有兩個狀況:


1. TWebResponse.ContentType 只會抓一組設定

往往後端回傳完整 HTML 頁面時,ContentType 會設定如下:

text/html; charset="utf-8"

但在讀取 ContentType 時卻只會讀到前面的【text/html】。

這也是內容處理錯誤的第一個問題。



2. SetContent 只吃 AnsiString


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
procedure TWebResponse.SetUnicodeContent(const AValue: string);
var
  AAnsiString, LContentType: AnsiString;
  Bytes: TBytes;
begin
  LContentType := 'CHARSET="UTF-8"';
  Bytes := EncodingGetBytes(LContentType, AValue);
  AAnsiString := AnsiString(TEncoding.UTF8.GetString(Bytes));
  SetContent(AAnsiString); // Alway only AnsiString... This is bug...
end; 


看到這裡就知完全無解,只能放任這個 Bug 繼續橫行,轉由 ContentStream 自行轉碼輸出的手段了。


所幸 Delphi 10.2 後已經解決這個問題,舊版本可以先參考本文的處理方式應急。

希望它能對你有用。 ^_^

============== 2021/10/05 更新 ==============

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
function MBCSString(const s: UnicodeString; CodePage: Word): RawByteString;
var
  enc: TEncoding;
  bytes: TBytes;
begin
  enc := TEncoding.GetEncoding(CodePage);
  try
    bytes := enc.GetBytes(s);
    SetLength(Result, Length(bytes));
    Move(Pointer(bytes)^, Pointer(Result)^, Length(bytes));
    SetCodePage(Result, CodePage, False);
  finally
    enc.Free;
  end;
end;

在 Delphi 裡使用【多位元組字元集(MBCS) 的支援】方案。

只要使用 Response.Content := MBCSString,可以確實解決此問題。

 

和你分享 😉

 

See also:



沒有留言:

張貼留言