Skip to content

Lessons and Gotchas

Hsiehting Lin edited this page Jul 13, 2026 · 1 revision

教訓與通則

這頁收錄的是與主題無關的工程通則——從本專案踩坑淬煉出來,但適用於任何「要從自動化程式複刻一個真實瀏覽器的已認證請求」的場景。每則的結構固定:通則 → 會咬你的地方(gotcha)→ 學到的做法,最後附一個本專案的具體錨點當證據。不熟本專案的人也能直接拿走這些原則。

適用情境:你想用腳本/server/agent 重現某個網站前端發出的 API 呼叫(提問、改權限、下訂單……),而這個網站有登入、有機器人防護、request body 有你沒完全掌握的欄位。以下四條是這類任務反覆出現的坑。


1. 要複刻請求,抓「當下即時封包」,別信舊 HAR 或 response 回存值

通則:重現一個 API 呼叫時,唯一可信的來源是現在、在真實瀏覽器裡、實際送出去的那個 request。用 CDP(Chrome DevTools Protocol)的 Network.getRequestPostData 抓 request 端的原始 bytes。

會咬你的地方

  • 舊 HAR 會過期。前端會悄悄改欄位——加一個新欄位、拿掉一個舊欄位——而你半年前存的 HAR 完全看不出來。照著舊 HAR 組請求,可能成功、可能被靜默降級、也可能被判為可疑。
  • response 回存的 body ≠ 你送出去的 body。伺服器常會正規化你送的值再存回。你若把 response 裡看到的欄位當成「該送什麼」,就會複製到伺服器改寫後的版本,而不是前端原本送的版本。
  • 同一欄位在 request 與 response 可能不同值:例如送 "prod",存回變 "minimal"——伺服器自行改寫。以 response 為準就錯了。

學到的做法

  • 在真實瀏覽器裡實際觸發一次該操作,用 CDP 抓 request 端的 postData,逐 byte 比對你程式產生的 body。
  • 把「這是某年某月的即時擷取」寫進程式碼註解與 changelog,讓下一個人知道基準點,也知道它會再漂移。
  • 別把「response 看得到的欄位」和「request 該送的欄位」混為一談。

本專案錨點:一次即時 CDP 擷取揭露 request body 相對舊 HAR 有三處漂移(新增了一個設定版本欄位、某快取欄位其實前端根本不送、還有欄位順序差異)。三處都只有在真實瀏覽器抓當下封包才看得到。


2. 要「byte-identical」,JSON 欄位順序也算數

通則:如果目標是讓你的 request body 和前端逐 byte 相同(為了不在指紋/簽章/WAF 規則上露出破綻),那麼欄位順序欄位有無同等重要。

會咬你的地方

  • JSON.stringify(及多數語言的等價序列化)依物件屬性的插入順序輸出。{a,b}{b,a} 是不同的字串,即使語意等價。
  • 「多送一個無害欄位」不是無害的——只要前端不送它,你送了就露餡。反之,前端有送而你漏送,也一樣。
  • 預設值也要對齊:前端送 false vs 你整個省略該欄位,是兩種不同的 bytes。

學到的做法

  • 以即時擷取的 request 為準,照它的欄位順序組 body;把「順序刻意對齊即時擷取」寫進註解,避免後人「順手排整齊」而破壞它。
  • 只在前端真的會送某欄位時才送它;前端省略的欄位,你也省略(做成 opt-in,只有呼叫方明確要求才加)。
  • 加一個小測試:把序列化結果和擷取到的字串做 === 比對,回歸時立刻抓到漂移。

本專案錨點:把某欄位移到另一欄位之前、並讓某個快取欄位變成「只有明確要求才送」之後,序列化結果對即時擷取的追問請求達成 byte-identical === true,並以單元測試釘住。


3. 有機器人防護時,讓請求「在真實已登入的分頁裡」跑,而不是從 server 直送

通則:面對 DataDome / Cloudflare / Akamai 這類機器人防護,最穩的路不是想辦法偽造,而是把請求塞進使用者真實、已登入的瀏覽器 session 去執行(透過擴充功能/relay),讓防護層看到的就是一個正常的人類分頁。

會咬你的地方

  • TLS/JA3 指紋:Node/Python 的 TLS 握手指紋和真瀏覽器不同,防護層即使你 header 全對也能認出來,回 403 挑戰頁。header 對不代表過得了。
  • 這種 403 不可靠地重試——猛打只會更慘,而且沒有純 client 端的繞法。
  • 偽造 UA/client-hints 若和實際指紋不一致,反而更可疑(例如宣稱 Safari 卻夾帶 Chromium-only 的 hint)。

學到的做法

  • 架一條 relay:本機 daemon 收請求 → 瀏覽器擴充功能在真實分頁內 fetch → 回傳結果。認證=使用者的瀏覽器登入,不需 API key、不需 headless。
  • relay 只監聽 localhost,擴充功能只跟目標站與本機通訊,把攻擊面壓到最小。
  • 指紋要「整組照抄」某個真實瀏覽器 profile,不要跨 profile 混搭 header。
  • 保留一條無防護時可用的直送 fallback(給 doctor/煙霧測試/單元測試),但清楚標示它平時會被擋。

本專案錨點oe_ask 的 POST 從 Node 直送會被 DataDome 以 TLS 指紋擋下;唯一可行路徑是瀏覽器擴充 relay——同一個請求在使用者登入的分頁裡送出就通過。整個架構(server → relay daemon → extension → 已登入分頁)就是為這件事而生。


4. 別在 client 端重造「伺服器已經幫你組好的狀態」

通則:串接一個有狀態的流程(多輪對話、購物車、工作流)之前,先確認哪些狀態是伺服器自己維護的。很多情況你只需要傳一個父物件 id,伺服器就會把完整歷史/上下文組起來——你不必也不該在 client 端重建它。

會咬你的地方

  • 想當然耳把整段歷史塞進 request,結果是多餘又脆弱:一旦伺服器對歷史格式的預期改變,你手工組的版本就爛掉。
  • 你 client 端組的歷史還可能和伺服器的權威版本不一致,引入難查的 bug。
  • 反過來的坑:以為「只傳 id」不夠、於是又補一堆欄位,破壞了本來乾淨的呼叫(見第 2 條)。

學到的做法

  • 先實測「最小請求」能走多遠:只傳父 id,看伺服器是否自行補齊上下文。多半可以。
  • 把「狀態的權威來源在伺服器」寫進文件,讓 client 保持薄。
  • 需要驗證時,抓伺服器回存的物件,確認它確實嵌入了你沒傳的歷史——這是伺服器自組狀態的證據。

本專案錨點:追問(follow-up)機制上,前端只送一個 original_article(父文章 id)、完全不帶 inline 歷史;伺服器自己把上一輪問答組進新文章的 history。因此 client 端要做的僅僅是傳對 id——這正是 oe_askoriginal_article_id 早已在做的事。


共通心法

把這四條收束成一句:複刻一個已認證的網頁請求時,「真實瀏覽器當下實際送出什麼」是唯一權威——連順序都照抄,能借真 session 就別偽造,能讓伺服器自組的狀態就別自己扛。 你的工作是忠實搬運那一個請求,不是重新發明它。

驗證這類工作的黃金標準也一致:拿即時擷取的請求,和你程式產生的請求做逐 byte 比對。相等,才算複刻成功。

相關頁面