Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

IE6-8 unable to download file from HTTPS site

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).

like image 258
Keith Avatar asked Oct 29 '12 10:10

Keith


People also ask

Why can't I download a file from a website?

This error means you don't have permission to download this file from the server. To fix, go to the website where the file is hosted. Check if you need to sign in (or provide some other authentication). If you can't sign in, contact the website or server owner, or try finding the file on a different site.

Why Internet Explorer cannot download files?

To download files, Internet Explorer must create a cache or temporary file. In Internet Explorer 9 or a later version, if the file is delivered over HTTPS, and any response headers are set to prevent caching, and if the Do not save encrypted pages to disk option is set, a cache file is not created.


2 Answers

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.

like image 123
Keith Avatar answered Oct 19 '22 21:10

Keith


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

like image 29
Juri Avatar answered Oct 19 '22 21:10

Juri