2

ASP.NET WebForm 在 IE 重複 PostBack 問題

 3 years ago
source link: https://blog.darkthread.net/blog/webform-ie-submit-twice/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
ASP.NET WebForm 在 IE 重複 PostBack 問題-黑暗執行緒

有支 ASP.NET WebForm 活化石程式近期常被投訴行為異常,症狀是按送出鈕後網頁顯示找不到該筆資料,但事後查詢已處理完成。調閱 Log,發現出錯當下都有兩筆連續 POST 記錄(發出時間相同),推測是 IE 瀏覽器(該網頁為 IE Only)因不明原因一次送出兩筆 POST, 第一筆處理成功後將資料自記憶體暫存區清除,第二筆執行時便會因找不到資料而出錯。

由於是偶發,頻率不高,且表單也成功送出並不影響流程,但使用者遇到難免疑惑或抱怨,體驗不佳,還是該被解決。

爬文找到不少 IE 會重複送出一次以上的案例,較常發生於 JavaScript 呼叫表單 .submit() 方法。問題網頁是使用 input type="submit" 實體鈕送單,但 ASP.NET WebForm 有個 __doPostBack 方法,WebResource.axd 引用的 JavaScript 函式或事件會觸發 __doPostBack,嚴格來說,也有透過程式呼叫 .submit() 的可能:

function __doPostBack(eventTarget, eventArgument) {
    if (!theForm.onsubmit || (theForm.onsubmit() != false)) {
        theForm.EVENTTARGET.value = eventTarget;
        theForm.__EVENTARGUMENT.value = eventArgument;
        theForm.submit();
    }
}

除了呼叫 .submit() 造成問題單,也不乏使用純 input type="submit" 或 button 引發重複送單的案例,例如:

綜合以上線索,不管是否與 __doPostBack() 有關,都有可能是踩到 IE 的雷了。推測問題 ASP.NET WebForm 在 IE 重複送出 POST 有兩種狀況:

  1. 按 input type="submit" 時因不明原因產生連點兩下的效果
  2. 按 input type="submit" 的同時有其他邏輯觸發 __doPostBack() 形成兩個 POST

因此,只要找到一種做法可以同時防堵上述兩種狀況,即可避免問題。當務之急是先做一個可以重現問題的實驗環境,才能驗測是否能修好問題:

<%@Page Language="C#"%>
<script runat="server">
    //TODO: 示範用,忽略記憶體清除作業
    static Dictionary<string, string> data = new Dictionary<string, string>();
    void Page_Load(object sender, EventArgs e)
    {
        if (Page.IsPostBack) 
        {
            var seq = Request["seq"];
            var mode = Request["mode"];
            data[seq] = (data.ContainsKey(seq) ? data[seq] : string.Empty) + DateTime.Now.ToString("ssfff") + "\n";
            switch (mode) 
            {
                case "Fail":
                    throw new ApplicationException("啊,挫賽! 玩壞了。");
                case "Delay":
                    System.Threading.Thread.Sleep(3000);
                    break;
            }
            Response.ContentType = "text/plain";
            Response.Write("*** Result ***\n" + data[seq]);
            Response.End();
        }
    }
</script>
<!DOCTYPE html>

<html>
<head>
    <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=7" />
    <style>
        input[readonly] { width: 2.5em; color: #666; background-color: #eee }
    </style>
</head>
<body>
    <form id="form1" method="post" runat="server">
        <asp:ScriptManager runat="server" />
        <input type="text" name="seq" value="<%=Guid.NewGuid().ToString().Substring(0, 4)%>" readonly />
        <select name="mode">
            <option>Succ</option>
            <option>Delay</option>
            <option>Fail</option>
        </select>
        <input type="submit" name="action" value="Submit" />
        <asp:DropDownList runat="server" AutoPostBack="True">
            <asp:ListItem>1</asp:ListItem>
            <asp:ListItem>2</asp:ListItem>                
            <asp:ListItem>3</asp:ListItem>
        </asp:DropDownList>
    </form>
</body>
</html>

實驗網頁運作原理為 GET 時在 input type="hidden" name="seq" 放入隨機序號,方便統計重複 POST 的次數。共有三種回應模式,Succ - 馬上回應、Delay - 延遲三秒回應(方便多點幾次送出鈕)、Fail - 拋出錯誤(用來測試如出錯能否重送)。後方的 asp:DropDownList 設定 AutoPostBack="True",會在 onchange 時觸發 __doPostBack(),故切換下拉選項會自動送出表單,用以驗證按送出鈕與 __doPostBack() 混合運作的情境。

在未加入防重送機制前,連續點擊送出鈕會產生多筆 POST:

點送出鈕再切換下拉選單觸發 __doPostBack() 也會重複 POST:

補充冷知識,重複 POST 送出表單時,前面的 POST 請求會被瀏覽器擱置或取消(但會在伺服器執行完),以最後一次傳回的結果為準:

配合 __doPostBack() 執行時會檢查 Form.onsubmit 的邏輯,我寫了一小段程式,加入 form1.onsumbit 事件(若原本已有 onsubmit,請自行串接),送單前先檢查 document.body.isPosting 屬性,若為 true 代表己送出 POST 請求在等回應,傳回 false 防止重複送單;若為 false 則設定 document.body.isPosting = true 阻止其他送出動作:

    <form id="form1" method="post" runat="server">
        <script>
            var body = document.body;
            body.isPosting = false;
            document.getElementById("form1").onsubmit = function() {
                if (body.isPosting) return false;
                body.isPosting = true;
                return true;
            };
        </script>
        <asp:ScriptManager runat="server" />
        <input type="text" name="seq" value="<%=Guid.NewGuid().ToString().Substring(0, 4)%>" readonly />
        <!-- 略 -->
    </form>

加入這段簡單的程式,如下圖所示,不管是連續點擊 Submit 或是搭配切換下拉選單,就只會有一筆 POST:

在濃濃思古幽情中,我又完成一項古蹟檢修。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK