有時候,Bug 并不體現在程序錯誤上,而是行為偏差。在一次常規功能測試中,我們發現移動端某個提交請求被觸發了兩次,雖然后端做了冪等處理,但頻繁請求仍可能帶來性能問題、錯誤日志膨脹、以及潛在副作用。
這類問題常被歸類為“無影響的冗余請求”,但我們決定徹查觸發路徑與請求內容差異,確保系統行為在各種網絡和設備條件下都能一致。
本文記錄了我們如何通過多個抓包工具協作,從客戶端真實行為開始,逐步確認問題成因并設計驗證手段。
問題現象簡述
目標接口為內容收藏操作接口。日志中部分用戶在點擊“收藏”按鈕后,同一秒鐘內發起了兩次 POST 請求,唯一標識相同,僅參數順序略有不同。重復請求并未導致業務錯誤,但觸發了重復打點和錯誤日志記錄。
第一階段:后端日志 vs 客戶端邏輯初步對比
后端日志已明確標出“某些用戶雙次請求”,但客戶端代碼中綁定事件的邏輯沒有重復。我們決定從“請求層”而不是“代碼邏輯”入手,驗證真實觸發過程。
第二階段:建立可觀測抓包環境
我們測試時使用三種終端:
- Windows桌面客戶端(基于Electron)
- Web頁面(H5)
- iOS App
分別構建抓包環境:
工具 | 使用場景 | 理由 |
---|---|---|
Charles | 抓桌面端和H5請求 | 快速配置代理,界面清晰 |
Sniffmaster | 抓取iOS端點擊收藏后的完整HTTPS數據 | 無需越獄,解密HTTPS請求結構 |
mitmproxy | 添加攔截腳本,記錄請求時間間隔與字段序列 | 便于分析參數排序邏輯 |
Wireshark | 捕獲低層網絡重傳/斷線重連可能 | 輔助驗證 TCP 層觸發行為 |
第三階段:抓取并對比請求數據
我們進行了5次不同網絡環境下的收藏請求測試,記錄結果如下:
- Web 和桌面端均只發出一次請求,數據結構一致;
- iOS 端出現2次請求現象:首個請求帶完整簽名,第二個僅延遲約300ms,結構略有差異;
- 重現過程中,App未卡頓、用戶未重復點擊。
使用 Sniffmaster 抓到的首個請求結構完整,攜帶認證信息;第二個請求字段順序變化、缺少特定 header,推測由緩存邏輯觸發。
我們進一步將這些請求導出,使用腳本對字段逐個比對:
# 簡化字段比對
def diff_keys(req1, req2):for k in set(req1) | set(req2):if req1.get(k) != req2.get(k):print(f"{k}: {req1.get(k)} != {req2.get(k)}")
結果明確指出第二次請求字段精簡,可能為“后補請求”或“失敗重發”。
第四階段:構造條件驗證自動重發機制
為了確認是否為 App SDK 中的重試邏輯觸發,我們借助 mitmproxy 添加延遲響應模擬:
def response(flow):if "/collect" in flow.request.url:import timetime.sleep(1.5) # 模擬服務端超時響應
添加該腳本后,App 端再次觸發重復請求,且第二次請求體與日志完全一致。驗證 App 在響應超過特定閾值后,自動重發該請求,說明問題并非“事件被綁定多次”,而是 SDK 邏輯層的超時重試機制未做去重處理。
第五階段:方案討論與結論輸出
我們最終定位問題源于:
- SDK 對超時請求未等待響應確認,直接補發;
- 接口未校驗是否已提交,導致業務處理走了兩次流程(雖然冪等性保障了數據不重復);
- 日志與埋點未做去重,導致“問題感知”加劇;
解決方案:
- 客戶端增加“是否發起中”標記,攔截重復點擊和補發;
- 后端加入請求ID機制,前端生成唯一標識避免冪等邏輯失效;
- 埋點與日志側加入重復判定邏輯,減少誤報;
抓包協作流程的價值復盤
單一工具并不能支撐從“重現現象”到“還原過程”到“構造條件”再到“驗證結果”的完整鏈條。我們每個階段只用了它最擅長的工具:
- Charles → 抓桌面、Web行為;
- Sniffmaster → 還原iOS端真實場景;
- mitmproxy → 攔截與干預請求邏輯;
- Wireshark → 輔助網絡層干擾排查;
這種“任務拆解式”調試方式,讓問題不是“抓到了”,而是“理解了”。
寫在最后
如果你也經常遇到“行為不一致但日志正常”的問題,不妨試著建立一套多工具協作的抓包分析流程。不是為了用工具炫技,而是為了讓每一個網絡行為都被還原、每一次異常都有解釋。
真正高效的調試流程,不靠“哪個工具最全”,而靠“流程拆得夠細、每步工具用得剛好”。