這週遇到個想不到的坑,特別來記錄一下。故事是這樣的--

在需要呼叫其他 API 服務時,發生了以下怪事:

  • 打某支查詢 API,突然查不到任何東西,或是跳出參數錯誤
  • 有些需要用參數組成 URL 的 API 跑出 Not Found
    • 第一組資料呼叫成功,第二組突然路徑錯誤
  • 寫入的時候,資料莫名其妙多了個 ?
    • 例如原先的資料是 ABC,不知怎地變成了 ABC?

由於這些操作都涉及到同一個參數,直覺上就是我們這邊給的參數出了點問題,馬上進入找犯人的環節。直接中斷點標記下去,反覆觀察該字串,但它就是一個普通的字串 "ABC",完全看不出什麼端倪。

正要覺得參數沒有問題的時候,赫然發現組出來的 Url 相當不對勁:在該參數的後方,多出了 %E2%80%8B 這串神秘東西!

當下我驚呆了,我們傳出去的 Url 裡,並不是預想的 /api/product/ABC,而是 /api/product/ABC%e2%80%8b!真是赤裸裸的背叛!這串鬼東西到底是什麼來頭?!

一查下去,原來這東西叫做 零寬空格(Zero-width space, ZWSP)

顧名思義,就是完全沒有寬度的空白字元。這東西在 Unicode 叫做 U+200B,我們比較常見到的是編碼之後的樣子 %e2%80%8b\xe2\x80\x8b。他還有另外兩個兄弟 U+200CU+200D,平常在泰文、高棉文之類的地方工作,這東西的特色就是:肉眼不可見、殺人於無形

它有多可怕,讓我們直接用 Linqpad 來試看看吧。

現在我們有選手 A 和選手 B,其中選手 A 偷偷嗑了禁藥 ZWSP:

var a = "ABC" + '\u200B';
var b = "ABC";

讓我們打印出來看看:

$"a:{a}".Dump();
$"b:{b}".Dump();

Image

看起來完全一模一樣,連反白都分辨不出來!

a.Length.Dump(); // 4
b.Length.Dump(); // 3

看來其中一個傢伙明顯比較長。

(a == b).Dump();      // False
(a.Equals(b)).Dump(); // False

看來這兩個傢伙完全不一樣!

目前看來,從長度和比較運算等方面都會發現它們並不一樣,但是肉眼卻分不出來。

這樣就會產生一些看起來像是 ("ABC" == "ABC") // False 的神奇場景,除了揉眼睛然後哭喊「明明就一樣」以外無從下手。更可怕的是當我們拿去組 Url,問題就出來啦:

HttpUtility.UrlEncode(a).Dump(); // ABC%e2%80%8b
HttpUtility.UrlEncode(b).Dump(); // ABC

真是完蛋。

同時在查資料的時候,也發現 NET Framework 3.5 之後的 Trim() 並不把這個零寬空格當成空白,因此單純用 Trim() 是不會把這鬼東西砍掉的。

所以如果你有以下情況,你可能是零寬空格的受害者!

  • 存到資料庫的資料莫名多一個 ?(有看不見或編碼錯誤的字元)
  • 呼叫 API 服務的時候,不是參數錯誤,就是直接報錯找不到(檢查組完的 URL)

這邊也順便記一下怎麼解決的,雖然是用相當暴力的方式:

(a.Replace("\u200B", "") == b).Dump(); // true

沒錯,我直接 Replace 掉它了囧,勉強度過了這次危機。

備註:如果有更好的處理方式,也歡迎提供給我呦,感謝~

雖然我更疑惑的是,資料裡面到底為啥會出現這種東西啦囧……

最後感謝一下這次讓我得到幫助的網路文章。每次 Debug 都要感謝前人們的禮物,謝謝。