在 .NET 裡要解析 URL 參數字串(QueryString,例如: a=1234&b=ABCD),自己拆字串就遜掉了,呼叫 HttpUtility.ParseQueryString() 才是王道,這是我很多年前就學到的知識。

最近再有個新發現,ParseQueryString() 所傳回的結果表面上是個 NameValueCollection,但骨子裡則是內部型別 – HttpValueCollection,它有個特異功能,ToString() 被覆寫成可將 Name/Value 再組合還原成 QueryString,所以我們可以用它解析 QueryString,增刪修改參數再 ToString() 轉回 QueryString,十分方便。

不過,試著試著踩到一顆地雷,它的 ToString() 處理中文編碼有問題:

static void TestParseQueryString()
{
	var urlQuery = "a=123&b=%E4%B8%AD%E6%96%87";
	var collection = HttpUtility.ParseQueryString(urlQuery);
	Console.WriteLine($"Query: {urlQuery}");
	Console.WriteLine($"ParseQueryString: a={collection["a"]},b={collection["b"]}");
	Console.WriteLine($"ToString: {collection.ToString()}");
}

實測解析內含 UrlEncode 中文參數再還原,可發現 HttpValueCollection.ToString() 傳回的不是 UTF-8 編碼 UrlEncode,而是過時 %uxxxx 格式 ! (延伸閱讀:【茶包射手日記】勿用 UrlEncodeUnicode 與 escape )

HttpValueCollection 原始碼也證實這點,註解提到是為了向前相容才繼續使用被標為過時(Obsolete)的 UrlEncodeUnicode 方法,而程式碼埋了一段偵測 AppSettings.DontUsePercentUUrlEncoding 設定改用 UrlEncode 的邏輯:

// HttpValueCollection used to call UrlEncodeUnicode in its ToString method, so we should continue to
// do so for back-compat. The result of ToString is not used to make a security decision, so this
// code path is "safe".
internal static string UrlEncodeForToString(string input) {
	if (AppSettings.DontUsePercentUUrlEncoding) {
		// DevDiv #762975: <form action> and other similar URLs are mangled since we use non-standard %uXXXX encoding.
		// We need to use standard UTF8 encoding for modern browsers to understand the URLs.
		return HttpUtility.UrlEncode(input);
	}
	else {
#pragma warning disable 618 // [Obsolete]
		return HttpUtility.UrlEncodeUnicode(input);
#pragma warning restore 618
	}
}

換言之,在 config 加入以下設定即可讓 HttpValueCollection 改用 UrlEncode:

  <appSettings>
    <add key="aspnet:DontUsePercentUUrlEncoding" value="true" />
  </appSettings>

實測成功!

不過,若是在共用函式或公用程式庫使用 HttpValueCollection,要求開發者修改 config 配合太擾民。故還有另一種解法,先用 ParseQueryString() 解讀為 NameValueCollection 後用 Uri.EscapeDataString() 以標準 UTF-8 編碼重新組裝:

 static void TestParseQueryString()
{
	var urlQuery = "a=123&b=%E4%B8%AD%E6%96%87";
	var collection = HttpUtility.ParseQueryString(urlQuery);
	Console.WriteLine($"Query: {urlQuery}");
	Console.WriteLine($"ParseQueryString: a={collection["a"]},b={collection["b"]}");
	//使用EscapeDataString重新編碼
	var fixedResult = string.Join("&", 
		collection.AllKeys.Select(key => key + "=" + Uri.EscapeDataString(collection[key])).ToArray());
	Console.WriteLine($"ToString: {fixedResult}");
}

這樣也能修正問題,報告完畢。


Comments

# by billhong

var fixedResult = Uri.EscapeUriString(HttpUtility.UrlDecode(collection.ToString())); 這個解法似乎沒有考慮 query parameter 若有包含需要被 url encode 的符號這種 cases 雖然解決了 中文字編碼的問題,但原本沒問題的部分卻出錯了 e.g. 原始 url 是 "a=%23123" (url 的 query parameter 代入 a=#123 其中 # 是需要被 url encode 的部分) 執行 var collection = HttpUtility.ParseQueryString("a=%23123"), collection 有正確的 url decode 得到 key="a" 和 value="#123" 但接下來重組回 url => Uri.EscapeUriString(HttpUtility.UrlDecode(collection.ToString())) 會得到 "a=#123" 不是原本正確的 url encode 的 "a=%23123"

# by Jeffrey

to billhong, 同意,應該用 Uri.EscapeDataString() 才夠嚴謹,已修改程式寫法,感謝指正。

Post a comment