avatar
蕭富云行動應用開發者 · Android & Flutter

離線失敗的請求,為什麼重連網路後又自己送出去了?

2026-06-17 · 8 min read

說明: 本文由 AI(Claude)根據我的初始筆記與想法完成。

這個問題困擾了我好幾年:一個理應失敗的網路請求,為什麼有時候在我把網路接回來之後,竟然自己送出去了?

最近在一個 Flutter app 上又遇到,這次總算把來龍去脈弄清楚了。結論很掃興——它不是什麼「重連自動同步」的聰明功能,而是一場跟 TCP 連線逾時賽跑的競態(race)。

症狀

App 是離線優先(offline-first)的:存一筆紀錄時,先寫進本地的加密資料庫,然後在背景「射後不理」地發一個 HTTP 上傳。離線也不會掉資料,上傳只是盡力而為。

(以下的測試與數字都是在 iOS 上量得的——連線逾時、負快取這些行為會因作業系統而異,別把秒數當成跨平台的定值。)

某次測試時,我看到這樣的畫面:

  1. 關掉 wifi,存一筆紀錄——上傳請求沒有馬上失敗,而是卡住
  2. 過了一陣子,我把 wifi 接回來,去做了一件完全無關的操作。
  3. 那筆原本「該失敗」的紀錄,自己上傳成功了

看起來就像 app 偵測到網路回來、然後自動補送。但我很確定——這個 app 裡根本沒有任何監聽連線狀態的程式碼。 那它到底是怎麼送出去的?

誤讀:「它一定有重連自動同步」

這是最直覺、也最錯的解釋。沒有 connectivity listener,就沒有人去觸發補送。請求不是被「重新發送」的——它從頭到尾就只發了一次,只是那一次拖了很久才完成。

關鍵在「拖了很久」這四個字。

真正的原因:請求一直在「等」,不是「失敗」

把一個 HTTP POST 在沒有網路時會發生的事拆開來看:

而 Dart 的 http.Client.post 預設沒有任何 app 層級的逾時,所以這個 Future 會一直 pending,整整等滿那段連線逾時的時間。

再加上請求是「射後不理」的(unawaited.then() 沒有 await),UI 早就往下走了,但那個請求還活在背景裡:

// 大意如此,不是實際程式碼
unawaited(client.post(uri, body: payload)); // 沒有 timeout,沒被 await
// ↑ 離線時,這個 Future 會卡在 OS 的連線逾時視窗裡,可能長達 30 秒

於是「魔法」就發生了:如果我在連線逾時到期之前把網路接回來,下一個重送的 SYN 終於收到了回應 → 連線建立 → 那個還在背景排隊的 POST 順利完成。

看起來像自動同步,其實只是趕在連線逾時前接回網路的一場賽跑

值得強調的是,這非常不可靠:只要你晚一步——重連時逾時已經到了——請求老早就失敗了,在有人明確去重試之前,什麼都不會上傳。

旁證:第一次慢、之後都秒失敗

還有一個線索很能佐證上面的機制:離線存第一筆,請求會卡大約 30 秒;但接著存第二、第三筆,卻是瞬間失敗

這是典型的負快取(negative caching):第一次連一個連不到的主機,要等滿整個 OS 連線逾時;一旦失敗,OS 會把「主機不可達/DNS 解析失敗」的結果快取起來,後續的嘗試就直接失敗、不再傻等。

第一次慢、其餘都快——這是網路堆疊的行為,不是 app 做了什麼。

附帶的一課:「有網路介面」不等於「能上網」

要判斷「離線」,我們用了 connectivity_pluscheckConnectivity()。但它讀的是作業系統的網路介面狀態,而不是真正能不能連到網際網路。

我就被這點咬了一口:在一台關掉 wifi、但用 USB 接到 Mac 做偵錯的 iPad 上,checkConnectivity() 回傳了 ConnectivityResult.other——那條 USB 偵錯線被算成一個「已連線」的介面——於是 app 以為自己在線上,照樣去跑上傳,然後就卡住了。我第一版的修法(判斷 any != none)正是因為這個原因而錯。

正確的做法是:只把真正能連外網的介面wifi / mobile / ethernet / vpn)算成在線,把 other / bluetooth / none 都當成離線。

教訓是:介面存在 ≠ 能上網。 connectivity_plus 回答的是「有沒有介面是 up 的?」,不是「我能不能連到網際網路?」。要問後者,得真的去探測一次(一個帶短逾時的 DNS 查詢或請求)——代價是用一點點延遲換掉「瞬間」的回應。

收尾:與其賭一個還沒斷的 socket,不如自己畫好界線

想讓離線行為變得確定、而不是去跟連線逾時賭運氣,方向其實很清楚:

三句話總結:把逾時的界線畫好,別相信一個還在苟延殘喘的 socket,而且「介面是 up 的」永遠不等於「連得上網際網路」。