-
Notifications
You must be signed in to change notification settings - Fork 14
Lessons and Gotchas
這頁收錄的是與主題無關的工程通則——從本專案踩坑淬煉出來,但適用於任何「要從自動化程式複刻一個真實瀏覽器的已認證請求」的場景。每則的結構固定:通則 → 會咬你的地方(gotcha)→ 學到的做法,最後附一個本專案的具體錨點當證據。不熟本專案的人也能直接拿走這些原則。
適用情境:你想用腳本/server/agent 重現某個網站前端發出的 API 呼叫(提問、改權限、下訂單……),而這個網站有登入、有機器人防護、request body 有你沒完全掌握的欄位。以下四條是這類任務反覆出現的坑。
通則:重現一個 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 有三處漂移(新增了一個設定版本欄位、某快取欄位其實前端根本不送、還有欄位順序差異)。三處都只有在真實瀏覽器抓當下封包才看得到。
通則:如果目標是讓你的 request body 和前端逐 byte 相同(為了不在指紋/簽章/WAF 規則上露出破綻),那麼欄位順序與欄位有無同等重要。
會咬你的地方:
-
JSON.stringify(及多數語言的等價序列化)依物件屬性的插入順序輸出。{a,b}和{b,a}是不同的字串,即使語意等價。 - 「多送一個無害欄位」不是無害的——只要前端不送它,你送了就露餡。反之,前端有送而你漏送,也一樣。
- 預設值也要對齊:前端送
falsevs 你整個省略該欄位,是兩種不同的 bytes。
學到的做法:
- 以即時擷取的 request 為準,照它的欄位順序組 body;把「順序刻意對齊即時擷取」寫進註解,避免後人「順手排整齊」而破壞它。
- 只在前端真的會送某欄位時才送它;前端省略的欄位,你也省略(做成 opt-in,只有呼叫方明確要求才加)。
- 加一個小測試:把序列化結果和擷取到的字串做
===比對,回歸時立刻抓到漂移。
本專案錨點:把某欄位移到另一欄位之前、並讓某個快取欄位變成「只有明確要求才送」之後,序列化結果對即時擷取的追問請求達成 byte-identical === true,並以單元測試釘住。
通則:面對 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 → 已登入分頁)就是為這件事而生。
通則:串接一個有狀態的流程(多輪對話、購物車、工作流)之前,先確認哪些狀態是伺服器自己維護的。很多情況你只需要傳一個父物件 id,伺服器就會把完整歷史/上下文組起來——你不必也不該在 client 端重建它。
會咬你的地方:
- 想當然耳把整段歷史塞進 request,結果是多餘又脆弱:一旦伺服器對歷史格式的預期改變,你手工組的版本就爛掉。
- 你 client 端組的歷史還可能和伺服器的權威版本不一致,引入難查的 bug。
- 反過來的坑:以為「只傳 id」不夠、於是又補一堆欄位,破壞了本來乾淨的呼叫(見第 2 條)。
學到的做法:
- 先實測「最小請求」能走多遠:只傳父 id,看伺服器是否自行補齊上下文。多半可以。
- 把「狀態的權威來源在伺服器」寫進文件,讓 client 保持薄。
- 需要驗證時,抓伺服器回存的物件,確認它確實嵌入了你沒傳的歷史——這是伺服器自組狀態的證據。
本專案錨點:追問(follow-up)機制上,前端只送一個 original_article(父文章 id)、完全不帶 inline 歷史;伺服器自己把上一輪問答組進新文章的 history。因此 client 端要做的僅僅是傳對 id——這正是 oe_ask 的 original_article_id 早已在做的事。
把這四條收束成一句:複刻一個已認證的網頁請求時,「真實瀏覽器當下實際送出什麼」是唯一權威——連順序都照抄,能借真 session 就別偽造,能讓伺服器自組的狀態就別自己扛。 你的工作是忠實搬運那一個請求,不是重新發明它。
驗證這類工作的黃金標準也一致:拿即時擷取的請求,和你程式產生的請求做逐 byte 比對。相等,才算複刻成功。
- Maintenance — relay daemon 的日常操作與疑難排解
- Plan and Tech Debt — 雙軌傳輸路徑等相關技術債
- Home — 架構總覽
OpenEvidence MCP
- Home(介紹)
- Maintenance(維護)
- Roadmap(路線圖)
- Plan and Tech Debt(計畫與技術債)
- Lessons and Gotchas(教訓與通則)
- Head First Software Architecture(架構觀)
外部連結