Skip to main content

MVC Bug: The virtual path '[path]' maps to another application, which is not allowed

I’ve being doing a lot of work on Asp.Net MVC (now v2) over the past few months and, firstly, I have to say that it totally rocks.

I will not go into any real detail about MVC here – this post is about the method System.Web.Mvc.HtmlHelper.AntiForgerytoken and that it highlights a bug in an internal class TokenPersister that you’re potentially going to get if you ever use spaces in your virtual directory names.  Ironically, the class in question is marked with a comment that says it’s difficult to unit test because of the way it fakes Asp.Net requests – ironic that it should be such a class in which we find a bug!  A better argument for unit-testing you’re unlikely to find.

Detail – Reproduce that bug

Using MVC it’s very easy to reproduce.  I should start by declaring my system configuration:

  • Windows 2008 R2
  • IIS 7.5 installed and configured
  • Visual Studio 2008 with all service packs etc
  • .Net 3.5 sp1 plus security patches

VS2010 and .Net 4 are also on this machine – and I would welcome anybody following the same steps to confirm that i:

  1. Create a new Asp.Net MVC2 Web Application (from the template) called ‘Asp Net Bug 2008’
    • Don’t bother with the unit tests project
  2. Open the project web properties, and set the project to use the Local IIS Web Server, the location should automatically be set to http://localhost/Asp Net Bug 2008/ (you might need to add the trailing slash here) – the spaces here are important so leave them in.
    • Create the Virtual Directory
    • Don’t forget to save the project file afterwards (like I just did as I created this walkthrough!)
  3. Open the view Views/Account/Register.aspx
  4. Just after the line
    1. <% using (Html.BeginForm()) { %>
    Add the following:
    1. <%= Html.AntiForgeryToken() %>
  5. Compile and run
  6. Navigate to the [Log On] link at the top of the page
  7. Hit the ‘Register’ link

You will see this exception helper:

 mvcexception

And then on the Asp.Net error page, you’ll see this stack trace:

[ArgumentException: The virtual path '/Asp%20Net%20Bug%202008/Account/Register' maps to another application, which is not allowed.]
System.Web.CachedPathData.GetVirtualPathData(VirtualPath virtualPath, Boolean permitPathsOutsideApp) +11193138
System.Web.HttpContext.GetFilePathData() +61
System.Web.Configuration.HttpCapabilitiesBase.GetBrowserCapabilities(HttpRequest request) +124
System.Web.HttpRequest.get_Browser() +168
System.Web.UI.Page.SetIntrinsics(HttpContext context, Boolean allowAsync) +207
System.Web.UI.Page.ProcessRequest(HttpContext context) +232
System.Web.Mvc.TokenPersister.CreateFormatterGenerator() +459
System.Web.Mvc.FormatterGenerator..cctor() +10

[TypeInitializationException: The type initializer for 'FormatterGenerator' threw an exception.]
System.Web.Mvc.AntiForgeryDataSerializer.get_Formatter() +38
System.Web.Mvc.AntiForgeryDataSerializer.Serialize(AntiForgeryData token) +181
System.Web.Mvc.HtmlHelper.GetAntiForgeryTokenAndSetCookie(String salt, String domain, String path) +405
System.Web.Mvc.HtmlHelper.AntiForgeryToken(String salt, String domain, String path) +13
System.Web.Mvc.HtmlHelper.AntiForgeryToken() +17
ASP.views_account_register_aspx.__RenderregisterContent(HtmlTextWriter __w, Control parameterContainer) in c:\Users\andras.zoltan\documents\visual studio 2008\projects\aspnetbug2008\aspnetbug2008\Views\Account\Register.aspx:17
System.Web.UI.Control.RenderChildrenInternal(HtmlTextWriter writer, ICollection children) +115
ASP.views_shared_site_master.__Render__control1(HtmlTextWriter __w, Control parameterContainer) in c:\Users\andras.zoltan\documents\visual studio 2008\projects\aspnetbug2008\aspnetbug2008\Views\Shared\Site.Master:26
System.Web.UI.Control.RenderChildrenInternal(HtmlTextWriter writer, ICollection children) +115
System.Web.UI.Control.RenderChildrenInternal(HtmlTextWriter writer, ICollection children) +240
System.Web.UI.Page.Render(HtmlTextWriter writer) +38
System.Web.Mvc.ViewPage.Render(HtmlTextWriter writer) +89
System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint) +4240

A closer look at this, you’ll see that the method GetBrowserCapabilities, called from the accessor of the HttpRequest.Browser is the culprit.  Opening Reflector, we see that this method is attempting to load the site configuration (web.config presumably) – which is where GetVirtualPathData comes in.  When I was confronted with this issue, it was obvious that the check which determines if the current request is in the current application root was failing.  So the first thing I thought was to change our app root to be something more friendly.


The Fix


Take the spaces out of the virtual directory name; perhaps replacing with hyphens, or just zeroing them down completely.  It’ll now work perfectly.


The Cause


Well, of course I haven’t analysed down to the last IL opcode here, but the first thing I thought was whether or not this was an Asp.Net bug, since the last few items in the call stack are from there.  So you can try another test, if you simply add a standard WebForm to the project (making sure it’s hosted in a vdir again that has spaces in it), and put in the following:





  1. <%= (Request.Browser!=null).ToString() %>

Compile and run that, and you’ll correctly get ‘True’ output in the page.  So, when the current HttpRequest is produced from the normal Asp.Net pipeline, everything works correctly.


So, let’s take another look at that stack trace – we have the call to

System.Web.Mvc.TokenPersister.CreateFormatterGenerator
Which the yields a call to
System.Web.UI.Page.ProcessRequest

But we’re already in that method further down, so why?


The reason can be found if you open CreateFormatterGenerator, either by getting the source code from CodePlex or by opening reflector.  Let’s take a look at the method body (which I have ripped straight out of the published project from CodePlex):





  1. public static Func<IStateFormatter> CreateFormatterGenerator()
  2. {
  3.     // This code instantiates a page and tricks it into thinking
  4.     // that it's servicing a postback scenario with encrypted
  5.     // ViewState, which is required to make the StateFormatter
  6.     // properly decrypt data. Specifically, this code sets the
  7.     // internal Page.ContainsEncryptedViewState flag.
  8.     TextWriter writer = TextWriter.Null;
  9.     HttpResponse response = new HttpResponse(writer);
  10.     HttpRequest request = new HttpRequest("DummyFile.aspx",
  11.         HttpContext.Current.Request.Url.ToString(), "__EVENTTARGET=true&__VIEWSTATEENCRYPTED=true");
  12.     HttpContext context = new HttpContext(request, response);
  13.  
  14.     Page page = new Page()
  15.     {
  16.         EnableViewStateMac = true,
  17.         ViewStateEncryptionMode = ViewStateEncryptionMode.Always
  18.     };
  19.     page.ProcessRequest(context);
  20.  
  21.     return () => new TokenPersister(page).StateFormatter;
  22. }

Well, the embedded comment there explains what’s going on – and it’s this fake request that’s causing the problem, clearly.  Whatever it is that Asp.Net does to make sure we don’t get this error on a standard WebForm is not being done properly – although I’m not sure what that could be.  My initial guess is that the %20 is being compared to the space in the path name, rather than being url-decoded first – and therefore a simple fix might be to change the call to HttpContext.Current.Request.Url.ToString() on the fourth code line above to pass in the url-decoded string.  If I was feeling ambitious, I’d hack into the project and pursue a fix.  However, my primary feeling is to report it to the Mvc team – which I have done here.

Comments

Popular posts from this blog

Asp.Net 2 and 4 default application pool generates CS0016 IIS7.5

Before I start – if you’ve found a bunch of other articles about this around the net, tried the fixes that are mentioned and still not getting any joy – then read on – you might find this solves your problem. Earlier today I discovered that when I run any ASP.Net 2 or 4 application through IIS7.5 using the default application pools (which use ApplicationPoolIdentity) on Windows 2008 R2 x64 I get an error message similar to this:   Server Error in '/MvcApplication31' Application. Compilation Error Description: An error occurred during the compilation of a resource required to service this request. Please review the following specific error details and modify your source code appropriately. Compiler Error Message: CS0016: Could not write to output file 'c:\Windows\Microsoft.NET\Framework64\v4.0.30319\Temporary ASP.NET Files\mvcapplication31\222b4fe6\4e80a86\App_global.asax.clb4bsnc.dll' -- 'The directory name is invalid. ' Source Error: [No relevant source

Serializing to attributes in WCF with DataContractSerializer

It’s a common problem – you want to return an object from a WCF service as XML, but you either want, or need, to deliver some or all of the property values as XML Attributes instead of XML Elements; but you can’t because the DataContractSerializer doesn’t support attributes (you’re most likely to have seen this StackOverflow QA if you’ve done a web search).  Most likely you’ve then migrated all your WCF service code to using the XmlSerializer (with all the XmlElement/XmlAttribute/XmlType attributes et al) – and you’ve cursed loudly. Well, I’m here to rescue you, because it is possible – and the answer to the problem is actually inferred from the MSDN article entitled ‘ Types supported by the Data Contract Serializer ’. The example I’m going to give is purely for illustration purposes only.  I don’t have a lot of time, so work with me! Create a new Asp.Net WCF service application, you can use Cassini as your web server (probably easier – otherwise you might have to enable Asp.Net

Shameless plug - Use the new JobServe Web API to search for jobs your way

As my signature states - I work for JobServe in the UK.  Over the past few months I have been working on a new REST API (using the ASP.Net Web API from pre-beta to RTM) and it is now in official public beta. Now of course, this isn't just so your good selves can start running your own job searches using whatever web-enabled client takes your fancy, but that is one of the cool benefits that has come out of it - and that's why we're calling it a public beta. At the time of writing, you can use the API to run almost any job search that you can on the website.  As you would expect, you can get the results in XML or JSON (also GET requests with search parameters in the query-string are supported as well).  Note that JSONP is not currently supported - but it's slated for a future release. Sounds great, how do I get in? In order to get cracking with this - you need to request an API token .  This is not an automated process but we should be able to get you set up in a