首页 > 解决方案 > 从控制器操作下载文件损坏

问题描述

我正在生成一些 JSON 内容,然后 GZipping 该内容,然后从 MVC 控制器操作将 gzip 压缩的内容返回给用户。

内容的生成和 gzipping 工作正常,因为我可以将生成的文件输出到磁盘,然后我可以使用 GZip 打开该文件。但是,当内容返回到浏览器时,内容已损坏。

我尝试了几种不同的方法来将内容返回到浏览器,例如

return File(byte[], "application/gzip");

return new FileStreamResult(stream, "application/gzip")

并且还使用 BinaryWrite() 和 WriteFile() 方法直接写入响应

无论我做什么,我在浏览器中收到的文件都是损坏的。

此代码显示了我当前尝试返回文件内容的方式。

// This line writes my content byte[] array to disk.  This file when opened with gzip works fine.
System.IO.File.WriteAllBytes(@"C:\temp\test.vcp", result.FileBytes);


// Writing out the byte array to the Response results in a corrupt file.  I have also attempted to Response.WriteFile(@"C:\temp\test.vcp") which also results in a corrupt file.

Response.Clear();
Response.ContentType = "application/gzip";

Response.AppendHeader("Content-Disposition", cd.ToString());
Response.AddHeader("Content-Length", result.FileBytes.Length.ToString());

Response.BinaryWrite(result.FileBytes);
Response.Flush();
Response.Close();
Response.End();

由于我正在创建的文件可以写入磁盘,并且可以使用 Gzip 读取,但浏览器接收到的文件已损坏,因此我确信我的文件创建正常。但不知何故,在将文件写入响应之后,它被损坏了。

我确实想知道是否某种 HTTPHandler 正在操纵结果,但我没有添加任何处理程序(我可以看到)。

我目前正在通过 IISExpress 在本地运行该应用程序。如何检查哪些 HttpHandlers/HttpModules 应用于管道?

最终,我希望在我的浏览器中收到与写入磁盘完全相同的文件。

作为参考,我生成的内容长度为 132 字节,但浏览器接收 216 字节。我注意到在查看接收数据的字节结构时,内容中有 3 个字节的重复模式,值分别为 239、191、189。看起来结果字节数组已被这些填充或填充3 个字节。

编辑

这是一个演示该问题的独立 Action 方法。

    [HttpGet]
    public void GetFile()
    {

        byte[] text = Encoding.ASCII.GetBytes(@"{""PetName"":""Doggy McDocFace"",""OwnerName"":""Kurt""}");
        byte[] compressed = Compress(text);

        var cd = new System.Net.Mime.ContentDisposition
        {
            // for example foo.bak
            FileName = "ExampleFile.vcp",

            // always prompt the user for downloading, set to true if you want 
            // the browser to try to show the file inline
            Inline = true,
        };


        System.IO.File.WriteAllBytes(@"C:\temp\ExampleFile.vcp", compressed);


        Response.Clear();
        Response.ContentType = "application/gzip";

        Response.AppendHeader("Content-Disposition", cd.ToString());
        Response.AddHeader("Content-Length", compressed.Length.ToString());

        Response.BinaryWrite(compressed);
        Response.Flush();
        Response.Close();
        Response.End();
    }

    public byte[] Compress(byte[] raw)
    {
        using (var memory = new MemoryStream())
        {
            using (var gzip = new GZipStream(memory, CompressionMode.Compress, true))
            {
                gzip.Write(raw, 0, raw.Length);
            }
            return memory.ToArray();
        }
    }

在这里,我欺骗了我的 JSON 内容,然后对其进行压缩。写入磁盘的文件工作正常,可以用我的 GZip 应用程序打开(我使用 7-zip)。但是,浏览器收到的文件已损坏。7-zip 无法将其识别为 gzip 文件。

编辑 2

所以看起来(感谢@Will)写入响应时的内容与 UTF-8 编码相冲突。我无法弄清楚如何,就像上面的示例一样,我正在使用 Encoding.ASCII.GetBytes() 将我的字符串转换为 byte[] 数组。

我试过设置

       Response.Charset = Encoding.ASCII.EncodingName;
       Response.ContentEncoding = Encoding.ASCII;

但这仍然不会导致下载有效文件。

编辑 3

我已将问题缩小到数据的 GZip 加密。如果我不加密数据,那么纯文本文件可以正常下载。但是,加密 byte[] 数组然后将该 byte[] 数组写入 Repsonse 会导致看起来像 UTF-8 编码的问题。任何值超过 127 的字节都会被我进一步提到的 3 个字节损坏。我无法弄清楚为什么响应以这种方式处理这些加密数据。我的假设是,当 Byte[] 数组只是作为 byte[] 数组的纯文本时,则处理得很好。只要它是一个正确的 byte[] 数组,即不仅仅是一个作为 byte[] 数组的字符串,那么响应中就会进行一些其他的编码转换。

标签: c#asp.net-mvc

解决方案


您可以尝试ActionFilterAttribute 基本上响应过滤器会在写入时查看响应输出流并转换流经它的数据。

ASP.NET MVC 中的 GZip/Deflate 压缩

public class CompressAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {

        var encodingsAccepted = filterContext.HttpContext.Request.Headers["Accept-Encoding"];
        if (string.IsNullOrEmpty(encodingsAccepted)) return;

        encodingsAccepted = encodingsAccepted.ToLowerInvariant();
        var response = filterContext.HttpContext.Response;

        if (encodingsAccepted.Contains("deflate"))
        {
            response.AppendHeader("Content-encoding", "deflate");
            response.Filter = new DeflateStream(response.Filter, CompressionMode.Compress);
        }
        else if (encodingsAccepted.Contains("gzip"))
        {
            response.AppendHeader("Content-encoding", "gzip");
            response.Filter = new GZipStream(response.Filter, CompressionMode.Compress);
        }
    }
}
[Compress]
[HttpGet]
public ActionResult GetFile()
{...}

推荐阅读