Thursday, January 29, 2009

ASP.NET AJAX Compatibility Patch for Safari 3.x and Google Chrome

The problem I am going to talk about is not new but it somehow managed to concern not so many developers so far. However it is a growing concern and more people would need a solution at some point.

The heart of the problem is there are two browsers with a groing number of users - Safari 3.x and Google Chrome, that ASP.NET AJAX framework is not compatible with. One part of the problem is induced by a fact that both browsers report themselves as "Webkit" which is not supported by ASP.NET AJAX. This part of the problem affects both Safari and Chrome users. The second part of the problem mostly affects Safari users because ASP.NET AJAX framework does "support" Safari (version 2.x) but in such a manner that makes your web application look a total disaster in Safari version 3.x. The suggested fix will help both cases. At least my tests showed quite a success.

Ideally we would need an official patch from Microsoft but since they have their own plans and busy with the next .NET/ASP.NET 4.0 and VS 2010 I would assume that there will be no fix till those versions are out. So let's stick to this solution for now.

The solutions for the problem were suggested by a couple of people: http://forums.asp.net/p/1252014/2898429.aspx and http://blog.lavablast.com/post/2008/10/Gotcha-WebKit-(Safari-3-and-Google-Chrome)-Bug-with-ASPNET-AJAX.aspx. Since both solutions are absolutely the same I want to give both of them a credit.

So to the point, ASP.NET AJAX framework has a class Sys.Browser that represents a current browser and supports cross-browser compatibility for everything else. The solution simply extends the Sys.Browser class to support a Webkitbrowser:

Sys.Browser.WebKit = {};
if( navigator.userAgent.indexOf('WebKit/') > -1 ) {
Sys.Browser.agent = Sys.Browser.WebKit;
Sys.Browser.version = parseFloat(navigator.userAgent.match(/WebKit\/(\d+(\.\d+)?)/)[1]);
Sys.Browser.name = 'WebKit';
}

That is all the code you need. To apply this code to your web application you will need to create a javascript file, let's say webkit.js and reference it in your application using standard ScriptManager server control. You can reference the fix as a file using this syntax:

<asp:ScriptManager ID="sm" runat="server">
<Scripts>
<asp:ScriptReference Path="~/Scripts/webkit.js" />
</Scripts>
</asp:ScriptManager>

or embed it into an assembly (my preferred way) and reference it from the assembly as an embedded resource:

<asp:ScriptManager ID="sm" runat="server">
<Scripts>
<asp:ScriptReference Assembly="Scripts" Name="Scripts.webkit.js" />
</Scripts>
</asp:ScriptManager>

Note that when you reference the fix from an assembly don't forget to add an assembly default namespace to the file name in the Nameattribute.

In conclusion, according to my tests the fix eliminated problems with UpdatePanel, AjaxControlToolkitand Virtual Earth map control.

Thursday, January 15, 2009

Search Engine Friendly Error Handling

ASP.NET provides a standard way to handle errors in web applications by configuring a customError section in the Web.config file. Standard behaviour is to redirect a user from an erroneous page to an error page that in its turn shows some kind of more or less friendly explanation of what has happened.

This approach is quite OK for intranet web applications but usually is not appropriate for a public web app accessible for search engines.

The main problem here is that when error happens ASP.NET returns 302 "temporary redirect" response redirecting browser to the error page configured in the Web.config.

If the erroneous page was accessed by a SE bot the bot would index a content of the error handling page under the original page's URL thus creating a wrong index entry.

Another problem is if the actual error on the page was that the requested content was not found. This scenario is quite regular on modern dynamic web sites that construct content pages dynamically based on a Url. In http world such a situation should be called as 404 "Not found" especially in the case of a bot visiting such a page. But ASP.NET standard handling will once again respond 302 and let the bot include a not existing page in a search index.

What I am driving at is the correct error handling should always return a corresponding http status code for a SE bot with the appropriate content for the user. So how can we do that without writing too much code?

Actually quite easy. Starting version 3.5 SP1 ASP.NET has a new attribute redirectMode in the customError configuration section:



The new redirectMode attribute can be assigned one of two values: ResponseRedirect (default) or ResponseRewrite. ResponseRewrite prevents the erroneous page being redirected with the code 302 to an error page however the content of the error page will be shown on the original page. Internally ASP.NET is doing Server.Execute of the error page instead of Response.Redirect as usual.

So this is already a one third of work. The next thing we need to do is to return a correct http status code. It can be achieved by adding a few lines of code to the error page let's say to a Page_Load event handler:

       int httpCode = 500;
       Exception ex = Server.GetLastError();
       if (ex is HttpException)
       {
           httpCode = ((HttpException) ex).GetHttpCode();
       }
       Response.StatusCode = httpCode;


This code checks if the reason of the error was an HttpException (a standard .NET exception class) then it assigns an http code from the exception otherwise it returns 500. Now it's two third of work done.

What left is to handle exceptions in your code properly. The best practices here are:
  • If your application can not return a requested content for any reason except your application's internal problems then throw an HttpException with the code 404 and it will be friendly handled by your error page.

    throw new HttpException(404, "Not found");

  • Map other your application's specific exceptions to standard  http codes and return them too.
  • If your application throws an exception internally then wrap the internal exception with HttpException using an appropriate http code.

    try
    {
    ...
    }
    catch (Exception ex)
    {
    throw new HttpException(code, message, ex);
    }
That's basically it. In conclusion just a few more notes.

If an erroneous page happened to start rendering content before the error occurred you may want to clear it on the error page before outputting an error message:

Response.Clear();

If for some reason you can not use the new redirectMode attribute in the .config file (older framework, application specifics, etc.) then just add a few lines of code to the global.asax that do the same:

void Application_Error(object sender, EventArgs e)
{
// Do something with the error, i.e. log, notify, etc. 
Server.Transfer(errorpage);
}

It may be a good idea to clear an error status on the error page after you're done with error handling:

ClearError();

Now that is all.