Rails tips: 怪異的ActionController::InvalidAuthenticityToken 錯誤

序:
最近在做醫療相關的數據應用,知道治病要找到病因,纔好對症下藥,因爲不同的病因,有可能引發相同的病症。而從症狀入手確定病因,卻也是最直接最常用的一個種診斷手法,但不能浮於表面現象,還要深入排查尋找病根才能真正根除疾病。

概述:
開始以爲是 protect_from_forgery 引發的問題,但實際卻是cgi接口的問題。由於“症狀”只有在特定環境纔會發生,在本地開發模式無法重現錯誤,更不用說調試,這纔是最麻煩的地方。有BUG不怕,怕的是找不到BUG在哪。

正文:
具體“症狀”表現爲:
在production server中,有時在進行登錄或表單提交操作時會出現異常提示:
ActionController::InvalidAuthenticityToken

異常拋出在:
/usr/local/lib/ruby/gems/1.8/gems/actionpack-2.3.4/lib/action_controller/request_forgery_protection.rb:79:in `verify_authenticity_token'


出錯後刷新幾次或者點擊別的頁面再重新提交問題消失不再出現。

因爲只是簡單的表單提交操作,因此並不是常見的ajax引發的InvalidAuthenticityToken 問題。即使把本地開發環境(apache+passenger,或者單個mongrel by script/server)切換到production 模式,並把production的數據在本地導入,都無法重現問題,似乎只在production server的運行環境(nginx + mongrel cluster)中才會出現。

要debug,肯定要找出bug所在,找出bug之前,就要確定bug出現的條件(現場重現)。

這個現象不是每次訪問都出現,感覺像是“隨機”出現,一時抓不到明確的規律。

不斷測試後來發現問題出現的共通特點:是在本地提及了代碼後,然後執行服務器自動更新命令(服務器自動更新代碼、數據遷移、最後重啓web server)後纔出現的。再一步測試,確認了是重啓web server後纔出現問題,“隨機”之謎破解。

有時按Ctrl+F5 強制刷新後問題消失,開始以爲是瀏覽器的自動填充表單中的authenticity_token 導致的,但測試所有瀏覽器後(FF3,CHROME,IE8,SeaMonkey)都有同樣現象。

反覆測試,終於找出了問題重現的步驟:

1. 打開一個show 頁面
2. 重啓mongrel server
3. 在show頁面進入編輯頁面
4. 提交表單
5. 出現錯誤後,後退到編輯頁,刷新(直接F5,或者Ctrl+F5強制刷新)後再提交,則不會出現報錯
(或者在第4步中,刷新兩次後再提交表單,則不會出現錯誤)

問題得以重現!接下來就可以開始debug了,但這情況debug有點麻煩。

多次查看HTML的源碼,發現authenticity_token的值並沒有變動(authenticity_token  這個值不是每次請求後都改變,而是在seesion的生命週期內有效)。

只好在test server(與production server完全一樣的運行環境:nginx + mongrel cluster)中進行跟蹤。
因爲沒辦法確定bug的確切引發因數,也沒辦法使用debugger觸發斷點,只好插入logger來跟蹤數據:
最接近異常的地方是 verified_request? 方法,因此在application_controller 中覆蓋它並插入跟蹤代碼:

def verified_request?
logger.info("form_authenticity_token=#{form_authenticity_token}")
logger.info("params_token:#{params[request_forgery_protection_token]}")
logger.info("params:#{params.inspect}")

!protect_against_forgery?     ||
request.method == :get      ||
request.xhr?                ||
!verifiable_request_format? ||
form_authenticity_token == params[request_forgery_protection_token]
end

在verified_request?插入日誌跟蹤發現,重啓mongrel後,提交的數據中POST的數據爲空(params中只有記錄到從 URL中獲取到的數據如controller,action,而沒有所提交的form數據),自然就無法通過 form_authenticity_token == params[request_forgery_protection_token] 這步判斷。

這時推測原因可能是 mongrel進程 重啓後,與nginx失去聯繫(雖然通訊端口不變),提交的數據由nginx接收到,但沒有全部轉發給mongrel (POST的數據丟失)

但查看nginx的日誌時,卻發現是有記錄到post數據的長度(長度等於在Firebug中發送出去的數據長度),因此確定nginx是有接收到post的數據,但沒有轉發給mongrel(或者mongrel接收出錯?)

至此,可以排除了是由 authenticity_token 相關代碼導致的問題,而是nginx和mongrel的通信問題導致了問題出現,因此才無法在本地重現問題。

知道關鍵部分所在,很快就能google到相關的問題 :
“   After you restart a Mongrel server the first request will always fail to have the correct HTTP request body. The reason for this is the override of CGI#initialize_query method by ActionController in the cgi_ext/query_extension.rb file.“

解決倒是簡單:
“but the solution is easy: just require action_controller/cgi_ext.rb when ActionController loads.“

這裏有該問題詳細的講解。

最終使用的解決方案是:
添加啓動補丁:
config/initializers/rails_patch.rb

內容爲:
require 'action_controller/cgi_ext' if Rails::version == '2.3.4'

問題解決!(該問題會在Rails 2.3.5 中解決)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章