日本做曖小視頻在線觀看網(wǎng)站百度app下載安裝 官方
在 Tubi,我們使用 Node.js 為 Web/OTT 應(yīng)用進(jìn)行服務(wù)端渲染及代理請求。近來,為了從新版本的性能改進(jìn)和新功能中受益,我們將 Node.js 從 14.x 版本升級到了 20.x。
升級像 Node.js 這樣的基礎(chǔ)設(shè)施絕非易事,尤其是有著許多第三方依賴的大型項目,因為我們無法預(yù)測可能會出現(xiàn)的問題。在升級過程中,我們很快便遇到了第一個問題:服務(wù)器端發(fā)送的每個請求都會失敗,并返回 404 的錯誤。
調(diào)試
第一個被懷疑的對象是倉庫里已經(jīng)逐漸被我們棄用的 request 庫。我們在 Node.js 20 環(huán)境下使用 request 庫發(fā)送請求復(fù)現(xiàn)了該問題:request.get('https://foo.bar/get'),服務(wù)端返回了包括 404 響應(yīng)在內(nèi)的多種錯誤。該庫使用了 Node.js 內(nèi)置的?http(s) 模塊來發(fā)送請求。為了探究發(fā)送請求時究竟發(fā)生了什么,我們又給測試添加了NODE_DEBUG=http,net 和 -trace-event-categories node.http,node.net.native 標(biāo)志。
{"cat":"node,node.http","name":"http.client.request","args":{"data":{"path":"/"}}}
以下是我們的發(fā)現(xiàn):
- 我們請求的是?https://foo.bar/get,但 http.request 的 path 卻是 /。
- 如果我們將 Node.js 降級到 14 版本并使用同樣的代碼,path 的值則是正確的 /get。這解釋了為什么服務(wù)器返回了 404 響應(yīng)。
我們嘗試在 Node.js 20 版本下構(gòu)造兩個最小復(fù)現(xiàn)案例: 一個案例采用了原生的 Node.js 模塊 http,另一個案例使用了 request 庫;但這兩個案例都無法復(fù)現(xiàn)這一異常情況。這讓我們開始懷疑,我們的代碼庫中是否有可能存在問題?
在深入研究代碼庫之前,我們在該庫 GitHub 的 issues 中發(fā)現(xiàn)了一個類似問題,其中提到了該問題可能與用于追蹤錯誤的?Sentry?SDK 有關(guān)。我們通過在 Node.js 20 中引入 Sentry 和該庫成功地復(fù)現(xiàn)了這一問題。那么,為什么 Request 庫 + Sentry + Node.js 20 會導(dǎo)致這一問題出現(xiàn)?我們來看看它們分別對 Node.js 的 http(s) 模塊做了什么。
調(diào)試內(nèi)置的 Node.js 模塊
為了檢查傳遞給 Node.js 內(nèi)置 http(s) 模塊的真實參數(shù),我們通過在命令中添加了 -expose-internals 參數(shù)(以及 -r internal/test/binding 標(biāo)志來訪問 primordials),并從?Node.js 代碼庫中復(fù)制了 http(s) 模塊文件:
// 將
const https = require('https');
// 修改為
const https = require('./path/to/local/https');
現(xiàn)在我們可以打印日志、添加斷點了,還可以直接修改模塊代碼。我們還發(fā)現(xiàn)了更多線索:http(s) 模塊接受?WHATWG URL 對象或一個普通對象作為參數(shù)。在我們的案例中,request 庫將一個普通對象作為參數(shù)發(fā)送給 http(s) 模塊,但由于某種原因,http(s) 模塊將其視為了 URL 對象。而如果它是一個 URL 對象,Node.js 會將其轉(zhuǎn)換為 http(s) 需要的參數(shù),并將 path 設(shè)置為 URL.pathname + URL.search。而這個庫傳遞的對象上不存在這些屬性,于是導(dǎo)致了真正發(fā)出的請求中 path 為空:
// The request function that http(s) module use:
// <https://github.com/nodejs/node/blob/36c72c8b2fb03414e02ffd8402c05129647ce123/lib/https.js#L367C5-L369>
function request(...args) {...if (isURL(args[0])) {options = urlToHttpOptions(ArrayPrototypeShift(args));}
}
// https://github.com/nodejs/node/blob/36c72c8b2fb03414e02ffd8402c05129647ce123/lib/internal/url.js#L1322C1-L1344
function urlToHttpOptions(url) {const { pathname, search } = url;const options = {...path: `${pathname || ''}${search || ''}`,}return options
}
那為什么 http(s) 模塊會將來自該庫的普通對象認(rèn)為是一個 URL 對象呢?在 isURL 函數(shù)中,如果在參數(shù)中找到 href 和 protocol 字段,則會認(rèn)為其是一個 URL 對象:
// <https://github.com/nodejs/node/blob/36c72c8b2fb03414e02ffd8402c05129647ce123/lib/internal/url.js#L761>
function isURL(self) {return Boolean(self?.href && self.protocol && self.auth === undefined);
}
那么這兩個屬性是如何被添加到傳遞給 http(s) 模塊的對象上的呢?我們檢查了庫和 Sentry 庫的源代碼,發(fā)現(xiàn) request 庫添加了 href 屬性,而 Sentry?添加了 protocol 屬性。
謎團(tuán)終于解開了!
如何修復(fù)這一問題?
我們理解了造成 404 錯誤的原因,也很清楚因為 request 庫、Sentry 和 Node.js 都存在各自的問題,所以修復(fù)這一問題將變得十分棘手;但問題的根源在于 Node.js 方面:isURL 函數(shù)無法準(zhǔn)確判斷是否為 URL 對象。因此,我們必須找到一種準(zhǔn)確判斷 URL 對象的方法。這應(yīng)該不會太難吧?
- 我們可以直接使用 object instanceof URL 嗎?很遺憾,結(jié)果是不行,因為它無法識別來自其他實現(xiàn)(例如 Electron)的 URL 對象。
- 我們是否可以檢查其他屬性,或者將屬性從 protocol 改回 origin 呢?雖然我們可以這樣做,但這并不是一個最理想的解決方案,因為其他屬性可能會引起性能問題。
我們無法找到一個最直接的解決方案,但我們注意到 isURL 函數(shù)上的另一個檢查 self.auth === undefined 解決了類似的問題;這就是為什么我們添加了遺留的 URL 對象屬性?self.path === undefined: path,而非 WHATWG URL 對象屬性。通過驗證 path 是否為 undefined 來確定是否為 WHATWG URL 對象,這是更合理的。在我們的案例中,path不是 undefined,這意味著該對象不是 WHATWG URL 對象。
于是我們決定在 GitHub 上開啟了一個討論,提交一個包含修復(fù)方案的 Pull Request,并對?Yagiz Nizipli 的這一評論做了深入思考:“這有點像雞生蛋還是蛋生雞的問題,我們既不能檢查 URL 對象,也不能正確檢查 url。我們唯一能做的就是檢查它們的行為 / 定義?!?/p>
好消息是,我們提出的修復(fù)方案將在 v20.6.0 版本中發(fā)布!對于那些繼續(xù)使用 v18.x 的用戶,這一修復(fù)方案很可能也會在?v18.18.0 版本中提供!
總結(jié)
Node.js、 request 庫和 Sentry 庫的變化導(dǎo)致 http(s) 模塊誤將一個普通對象識別為 URL 對象;這就是 http(s) 模塊重置了 path 并引發(fā)了問題的原因。以下是 Node.js 和這兩個庫近期實現(xiàn)的一些變更:
- Node.js 近期實現(xiàn)了將 URL 對象的檢測從帶有 origin 和 href 改為帶有 protocol 和 href 以提高性能。
- Request 庫向 http(s) 模塊的請求選項中添加了 href 屬性(或許是為了調(diào)試目的)。
- Sentry 庫中添加了一個 protocol 屬性以修復(fù)一個 bug。
我們的收獲
我們在解決了 Node.js 20 升級中出現(xiàn)的意外請求問題后,希望與大家分享這樣幾點收獲:
- 分而治之
由多個第三方庫交互而引發(fā)的問題可能很難調(diào)試與修復(fù),但有一種方法是從底層開始,逐步追蹤問題的源頭,將其分解為更小的問題,以更容易處理。
- 避免使用已棄用的庫
這相當(dāng)于在代碼中放置了一個定時炸彈,它最終一定會爆炸。我們在實踐中便遇到了這一問題。我們有計劃替換已棄用的庫,但在工作完成之前,這一定時炸彈就爆炸了。
- 避免 patch 原生模塊
在 Node.js 上 patch 原生模塊(如 http(s))會使調(diào)試變得更加困難,尤其是對于第三方庫,工程師們應(yīng)避免這種做法。
- 有時我們無法找到最完美的解決方案,我們能做的就是全力以赴。
Tubi 正在招聘
如你所見,Tubi 技術(shù)團(tuán)隊成功解決了在 Node.js 20 升級過程中出現(xiàn)的未預(yù)期的請求問題。我們快速鎖定了由第三方依賴引起的 URL 對象檢測變化是這一問題的根源。如果你和我們一樣對這一類問題的解決感興趣,歡迎加入我們!
Tubi 技術(shù)團(tuán)隊正在招聘,點擊此處可查看 Tubi 的熱招崗位。
作者:Zhuo Zhang, Tubi Senior Software Engineer