有支 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:

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

IE may submit the ASP.NET WebForm twice in one click, this article trys to reproduce the scenario and find the way to avoid it.


Comments

# by Slash

其實換一顆99元的滑鼠就解決了。

# by Jeffrey

to Slash, 原本有把滑鼠列為嫌犯,但有至少三名使用者報案,故先排除涉案可能。

# by Randy

近日有幸檢修公司古蹟,發現Edge與Chrome也有相同情況發生 感謝黑大無私地分享解救小弟~~

Post a comment