9

I have an MVC .Net application that has actions that return report files, usually .xslx:

byte[] data = GetReport();
return File(data, 
    "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", 
    "filename.xlsx");

This works great in testing and in all browsers, but when we put this on an SSL site it fails for IE6, 7 and 8 (all the proper browsers still work fine) with this unhelpful error:

Unable to download filename from server. Unable to open this Internet site. The requested site is either unavailable or cannot be found. Please try again later.

This used to work in a legacy application (non-MVC) that this action replaces.

We can't tell our users to change anything locally - about 60% are still on IE6!

How can I fix this using MVC?

Update

Further digging reveals that this is a fundamental failure in IE6-8. According to Eric Law's IE internals blog this happens because, during an SSL connection, IE treats the no-cache directive as an absolute rule. So, rather than not cache a copy, it considers no-cache to mean that it shouldn't be possible to save a copy to disk even when Content-Disposition:attachment and with an explicit prompt for a download location.

Obviously this is wrong, but while it's fixed in IE9 we're still stuck with all the IE6-8 users.

Using MVC's action filter attributes produces the following headers:

Cache-Control:no-cache, no-store, must-revalidate
Pragma:no-cache

Using Fiddler to change these on the fly we can verify the headers that need to be returned instead:

Cache-Control:no-store, no-cache, must-revalidate

Note the order of the Cache-Control must have no-store before no-cache and that the Pragma directive must be removed entirely.

This is a problem - we use MVC's action attributes extensively and I really don't want to rewrite them from scratch. Even if we could IIS throws an exception if you try to remove the Pragma directive.

How do you make Microsoft's MVC and IIS return the no-cache directive that Microsoft's IE6-8 can handle under HTTPS? I do not want to allow private caching of the response (as per this similar question) or ignore the MVC built in methods with an override (as per my own answer, which is just my current best hack).

Community
  • 1
  • 1
Keith
  • 150,284
  • 78
  • 298
  • 434
  • @tpeczek see update - I'm not looking to override MVC's action filters if at all possible, though I'll check that answer out. – Keith Oct 29 '12 at 11:03
  • @tpeczek also the answer to that question appears to be to use `Cache-Control:private`, which would then mean that if a user downloads the same file twice in the same session they'll get a copy from their local browser cache rather than the latest data. It's not a fix. I need to tell the browser not to cache (but do allow downloads) in a format that IE can understand – Keith Oct 29 '12 at 11:08
  • Let's just be clear about this question - I want to know how to make MVC output the cache directives that IE6-8 understands, **not** that making the cache private can have a similar effect. I have similar symptoms, but [Calin's question](http://stackoverflow.com/questions/9996353/internet-explorer-error-using-asp-mvc-4-0-fileresult) is no use to me. – Keith Oct 29 '12 at 17:17

2 Answers2

8

I've come up with a workaround, but it's a definite hack - this is a new cache attribute to replace the built-in [OutputCache] one:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
public sealed class IENoCacheAttribute : ActionFilterAttribute
{
    public override void OnResultExecuting(ResultExecutingContext filterContext)
    {
        if (filterContext.HttpContext.Request.IsSecureConnection &&
            string.Equals(filterContext.HttpContext.Request.Browser.Browser, "IE", StringComparison.OrdinalIgnoreCase) &&
            filterContext.HttpContext.Request.Browser.MajorVersion < 9)
        {
            filterContext.HttpContext.Response.ClearHeaders();
            filterContext.HttpContext.Response.AddHeader("cache-control", "no-store, no-cache, must-revalidate");
        }
        else
        {
            filterContext.HttpContext.Response.Cache.SetNoStore();
            filterContext.HttpContext.Response.Cache.SetCacheability(HttpCacheability.NoCache);
            filterContext.HttpContext.Response.Cache.SetRevalidation(HttpCacheRevalidation.AllCaches);
        }

        base.OnResultExecuting(filterContext);
    }
}

It's a workaround at best though - what I really want is to extend the existing [OutputCache] and Response.Cache structures so that they have the desired output suitable for legacy IEs.

Keith
  • 150,284
  • 78
  • 298
  • 434
2

I was having a similar approach in that I had a BaseController class

[OutputCache(Duration=0)]
public class BaseController : Controller
{
    //snip snip: some utility stuff and shared endpoints among all my controllers
}

That caused the above mentioned problems in IE8. Applying the [IENoCacheAttribute] as shown above didn't work however. The problem is that the instruction filterContext.HttpContext.Response.ClearHeaders() removes all of my headers including eventual Content-Disposition headers etc... causing the file download to not happen correctly.

My approach was therefore to overwrite the default OutputCacheAttribute.cs in such a way that in case of IE it didn't apply any kind of caching headers, especially the problematic no-cache ones.

public class EnhancedOutputCacheAttribute : OutputCacheAttribute
{

    public override void OnActionExecuted(ActionExecutedContext filterContext)
    {

        if (!IsFileResultAndOldIE(filterContext))
            base.OnActionExecuted(filterContext);
        else
        {
            //try the best to avoid any kind of caching
            filterContext.HttpContext.Response.Cache.SetCacheability(HttpCacheability.Private);
            filterContext.HttpContext.Response.Cache.SetMaxAge(new TimeSpan(0));
            filterContext.HttpContext.Response.Cache.SetExpires(DateTime.Now.AddMinutes(-5D));
        }
    }

    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        if (!IsFileResultAndOldIE(filterContext))
            base.OnActionExecuting(filterContext);
    }

    public override void OnResultExecuted(ResultExecutedContext filterContext)
    {
        if (!IsFileResultAndOldIE(filterContext))
            base.OnResultExecuted(filterContext);
    }

    public override void OnResultExecuting(ResultExecutingContext filterContext)
    {
        if (!IsFileResultAndOldIE(filterContext))
            base.OnResultExecuting(filterContext);
    }

    /// <summary>
    /// 
    /// </summary>
    /// <param name="filterContext"></param>
    /// <returns><c>true</c> for FileResults and if the browser is < IE9</returns>
    private bool IsFileResultAndOldIE(dynamic filterContext)
    {
        return filterContext.Result is FileResult &&
               filterContext.HttpContext.Request.IsSecureConnection &&
               string.Equals(filterContext.HttpContext.Request.Browser.Browser, "IE", StringComparison.OrdinalIgnoreCase) &&
               filterContext.HttpContext.Request.Browser.MajorVersion < 9;
    }

}

Here's the corresponding gist: https://gist.github.com/4633225

Juri
  • 32,424
  • 20
  • 102
  • 136
  • Very useful (and +1). Unfortunately, what with IE being so slow and only making two requests at a time I need it to cache more than the other browsers. – Keith Jan 25 '13 at 10:57
  • I've modified the solution here to also check for FileContentResult: bool result = (filterContext.Result is FileResult || filterContext.Result is FileContentResult) – stevieg Apr 03 '15 at 00:35
  • @stevieg: You don't have to check for FileContentResult explicitly, as it derives from FileResult. – Markus Wolters Sep 14 '15 at 10:06