User
art_snob encountered strange behavior of Android Web browser on his Mobile Web site. It took a while to find the true cause. It happens so, that the Android Web browser (at least up to OS ver. 2.3.3) tends to ignore the no-cache HTTP headers, and decides at any rate to avoid page reload. This causes severe problems with ASP.NET, which relies on synchronization of code-behind (e.g. a.aspx.vb) with the Web page (a.aspx).
Here is a stripped-down example:
a.aspx
<%@ Page Language="VB" CodeFile="a.aspx.vb" Inherits="a" EnableViewStateMac="False" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<HTML xmlns="http://www.w3.org/1999/xhtml">
<HEAD id="Head1" runat="server">
<TITLE>a.aspx</TITLE>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
</HEAD>
<BODY>
<FORM id="form1" runat="server">
<div id="container">
<asp:ScriptManager ID="ScriptManager1" runat="server">
</asp:ScriptManager>
<asp:UpdatePanel ID="UpdatePanel1" runat="server" UpdateMode="Always">
<ContentTemplate>
<asp:Button ID="ServerButton" runat="server" OnClick="Button_Click" text="go to b.html on server" /><p />
<asp:Button ID="ClientButton" runat="server" onClientClick="location = 'b.html'" text="go to b.html on client" /><p />
</ContentTemplate>
</asp:UpdatePanel>
</div>
</FORM>
</BODY>
</HTML>
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
Select allOpen in new window
a.aspx.vb
Partial Class a
Inherits Page
Protected Sub Button_Click(ByVal sender As Object, ByVal e As EventArgs) Handles ServerButton.Click
Response.Redirect("b.html", False)
End Sub
End Class
1:
2:
3:
4:
5:
6:
7:
8:
Select allOpen in new window
Now, on any known browser, the ServerButton and the ClientButton produce the same result:
b.html
<html>
<head>
<title>b.html</title>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
</head>
<body>
<input name="goback" onclick="history.back()" type="button" value="go back" /></p>
<input name="gotoA" onclick="location='a.aspx'" type="button" value="go to a.aspx" /></p>
<input name="replaceA" onclick="location.replace('a.aspx')" type="button" value="replace to a.aspx" /></p>
<input name="openA" onclick="window.open('a.aspx', 'top')" type="button" value="open a.aspx" /></p>
</body>
</html>
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
Select allOpen in new window
And on all known browsers, all four buttons in b.html display a.aspx again and let you repeat the process. Except goback button and the Android browser.
If you start at a.aspx and then click goback, or the system Back button, you will be still able to use the ClientButton, but the ServerButton will not work at all. Under the hood it will get an ASP.NET exception:
E/browser (xxx): Console: Uncaught Sys.WebForms.PageRequestManagerServerErrorException: Sys.WebForms.PageRequestManagerServerErrorException: An unknown error occurred while processing the request on the server. The status code returned from the server was: 0 http://xxx/ScriptResource.axd?d=31hrpqnrjCS7Ljksbfakhjr6vQHbaDyxah__zHgcOttSylOBq8Zbq2kxgVMdA3MThu4aquTF9WtwbL8sY19YcnLh9RlqCkMMQlaKeoC5pMVR-tgb3iU8M6inww0QtoQYLpIsOrH70stFuO0SE_LVrEyvT7miOJgBVPvhCD-3ekjwH95jk8pHDxgtqXOa4aGu0&t=2610f696:5
1:
Select allOpen in new window
I have explained above what causes this problem. Now our task is to find an easy and reliable plumbing.
I will use an XMLHttpRequest to verify that the copy of the page is not coming from local cache, but is fresh from the server. This way, we can guarantee that code behind will be live.
Here is the Page_Load procedure in a.aspx.vb:
Protected Sub Page_Load(ByVal sender As Object, ByVal e As EventArgs) Handles Me.Load
Dim uni = Request.QueryString("getUnique")
If uni = "" Then
HttpContext.Current.Session("QQQ") = Now().toString("ms")
Else
If (HttpContext.Current.Session("QQQ") Is Nothing)
uni = "9999"
Else
uni = HttpContext.Current.Session("QQQ")
End If
Response.Write(uni)
Response.Flush()
End If
End Sub
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
Select allOpen in new window
and here is the client-side javascript (put it somewhere inside a.aspx):
<script>
originalServerTime = <%= HttpContext.Current.Session("QQQ") %>
var request = new XMLHttpRequest();
request.onreadystatechange = function()
{
if (request.readyState == 4)
{
newServerTime=parseInt(request.responseText);
console.log("newServerTime: " + newServerTime + " originalServerTime: " + originalServerTime);
if (newServerTime != originalServerTime) // if not using Session persistence, allow for up to 2 sec delay
{
location.replace(location.href);
}
}
};
var ser = Math.round(Math.random()*1000000); // Anti-caching random number
request.open('GET', location.href + '?' + 'getUnique=' + ser, true);
request.send(null);
</script>
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
Select allOpen in new window
So, we generate a new request, make sure that it is never cached (with random getUnique), and retrieve the value that should be identical to the one embedded with <%= %> when the page was generated. If the values differ, the page has lost its connection to code behind.
We reload the page by force, and use location.replace() to replace the unhealthy page in the browser history.
Actually, you could choose to do this without using the Session object. Simply generate the server time on the fly in response to the request. Unfortunately, this approach is less robust: you must leave a couple of seconds "freedom" because the server may be too busy between the two requests, and the client may decide to press Back button really quickly.