如何在HTTP的Content-Disposition头中对文件名参数进行编码?

624

想要强制浏览器下载资源而不是直接在Web浏览器中呈现的Web应用程序,在HTTP响应中发出一个形式为:

Content-Disposition: attachment; filename=文件名

Content-Disposition标头。 filename参数可用于建议浏览器下载资源时使用的文件名称。然而RFC 2183(Content-Disposition)在section 2.3(The Filename Parameter)中指出,文件名只能使用US-ASCII字符:

当前[RFC 2045]语法约束 参数值(因此也就限制了 Content-Disposition文件名)为 US-ASCII。我们认识到允许文件名中使用任意字符集的优越性,但是本文档无法定义必要的机制。

尽管如此,有经验的证据表明,大多数流行的Web浏览器似乎仍允许使用非US-ASCII字符(由于缺乏标准),但它们对文件名的编码方案和字符集规范存在争议。问题是,如果需要将文件名“naïvefile”(不带引号,并且第三个字母为U +00EF)编码到Content-Disposition头中,则流行浏览器使用的各种方案和编码是什么?

对于这个问题,所谓流行的浏览器是:

  • 谷歌浏览器
  • 苹果Safari浏览器
  • 微软Internet Explorer或Edge浏览器
  • 火狐浏览器
  • 欧朋浏览器

已经在移动版Safari上运行成功(如@Martin Ørding-Thomsen所建议的使用原始UTF-8编码),但同一设备上的GoodReader无法正常工作。有什么想法吗? - Thilo
还可以参考这个类似的问题 - juergen d
1
Kornel的回答证明了这是最简单的方法,只要你可以设置路径的最后一段; 再加上 Content-Disposition: attachment - Antti Haapala -- Слава Україні
1
最新的RFC规范为**RFC 8187**,它取代了RFC 5987。 - Константин Ван
23个回答

429

我知道这是一篇旧文章,但它仍然非常相关。我发现现代浏览器支持rfc5987,它允许utf-8编码,百分号编码(url编码)。那么朴素的file.txt将变成:

Content-Disposition: attachment; filename*=UTF-8''Na%C3%AFve%20file.txt

Safari (5) 不支持此功能。相反,您应该使用 Safari 标准,在 utf-8 编码的标头中直接编写文件名:

Content-Disposition: attachment; filename=Naïve file.txt

IE8及更早版本也不支持此功能,您需要使用IE标准的utf-8编码,百分比编码:

Content-Disposition: attachment; filename=Na%C3%AFve%20file.txt

在ASP.Net中,我使用以下代码:

string contentDisposition;
if (Request.Browser.Browser == "IE" && (Request.Browser.Version == "7.0" || Request.Browser.Version == "8.0"))
    contentDisposition = "attachment; filename=" + Uri.EscapeDataString(fileName);
else if (Request.Browser.Browser == "Safari")
    contentDisposition = "attachment; filename=" + fileName;
else
    contentDisposition = "attachment; filename*=UTF-8''" + Uri.EscapeDataString(fileName);
Response.AddHeader("Content-Disposition", contentDisposition);

我使用IE7、IE8、IE9、Chrome 13、Opera 11、FF5、Safari 5进行了上述测试。

更新2013年11月:

这是我当前使用的代码。我仍然需要支持IE8,所以我不能去掉第一部分。事实证明,安卓浏览器使用内置的安卓下载管理器,在标准方式下无法可靠地解析文件名。

string contentDisposition;
if (Request.Browser.Browser == "IE" && (Request.Browser.Version == "7.0" || Request.Browser.Version == "8.0"))
    contentDisposition = "attachment; filename=" + Uri.EscapeDataString(fileName);
else if (Request.UserAgent != null && Request.UserAgent.ToLowerInvariant().Contains("android")) // android built-in download manager (all browsers on android)
    contentDisposition = "attachment; filename=\"" + MakeAndroidSafeFileName(fileName) + "\"";
else
    contentDisposition = "attachment; filename=\"" + fileName + "\"; filename*=UTF-8''" + Uri.EscapeDataString(fileName);
Response.AddHeader("Content-Disposition", contentDisposition);

上述内容现已在IE7-11、Chrome 32、Opera 12、FF25和Safari 6中进行了测试,使用以下文件名进行下载:你好abcABCæøåÆØÅäöüïëêîâéíáóúýñ½§!#¤%&()=`@£$€{[]}+´¨^~'-_,;.txt。

在IE7上,它适用于某些字符,但不适用于所有字符。但是现在谁还关心IE7呢?

这是我用来为Android生成安全文件名的函数。请注意,我不知道Android支持哪些字符,但我已经测试过这些字符肯定能正常工作:

private static readonly Dictionary<char, char> AndroidAllowedChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ._-+,@£$€!½§~'=()[]{}0123456789".ToDictionary(c => c);
private string MakeAndroidSafeFileName(string fileName)
{
    char[] newFileName = fileName.ToCharArray();
    for (int i = 0; i < newFileName.Length; i++)
    {
        if (!AndroidAllowedChars.ContainsKey(newFileName[i]))
            newFileName[i] = '_';
    }
    return new string(newFileName);
}

@TomZ: 我在IE7和IE8中进行了测试,结果发现我不需要转义撇号(')。你有什么失败的例子吗?

@Dave Van den Eynde: 将两个文件名组合成一行符合RFC6266的规定,但对于Android和IE7+8而言是不起作用的,我已经更新了代码以反映这一点。感谢您的建议。

@Thilo: 我不知道GoodReader或任何其他非浏览器的情况。按照Android的方法可能会有所帮助。

@Alex Zhukovskiy: 我不知道为什么,但如在Connect上讨论的那样,它似乎不能很好地工作。


1
搞定了移动Safari(按照上面建议的原始UTF-8编码),但同一设备上的GoodReader却不起作用。有什么想法吗? - Thilo
1
直接编写UTF-8字符似乎适用于当前版本的Firefox,Chrome和Opera。没有测试Safari和IE。 - Martin Tournoij
30
为什么不将它们组合起来,使用Content-Disposition: attachment; filename*=UTF-8''Na%C3%AFve%20file.txt; filename=Na%C3%AFve%20file.txt,并跳过浏览器嗅探呢?这样行得通吗? - Dave Van den Eynde
11
快递邮件的友好人员发现了另一个解决方法: https://blog.fastmail.com/2011/06/24/download-non-english-filenames/Content-Disposition: attachment; filename="foo-%c3%a4.html"; filename*=UTF-8''foo-%c3%a4.html文件名指定两次(一次不带UTF-8前缀,一次带)可以在IE8-11、Edge、Chrome、Firefox和Safari中正常工作(似乎苹果修复了Safari,所以现在也可以正常工作)。 - wullinkm
1
@MartinØrding-Thomsen 你知道为什么标准的 System.Net.Mime.ContentDisposition 会生成无效的名称,这些名称甚至不能被任何浏览器解释(包括 Chrome)吗? - Alex Zhukovskiy
显示剩余13条评论

187
  • Content-Disposition 中编码非 ASCII 名称的方式并不具有互操作性。浏览器兼容性混乱

  • Content-Disposition 中使用 UTF-8 的理论正确语法非常奇怪:filename*=UTF-8''foo%c3%a4(是的,那是一个星号,并且没有引号,除了中间的空单引号)

  • 这个头部有点不太标准(HTTP/1.1 规范承认其存在,但不要求客户端支持它)。

有一个简单而非常强大的替代方法: 使用包含所需文件名的 URL

当最后斜杠后面的名称就是您想要的名称时,您不需要任何额外的头信息!

这个技巧可以使用:

/real_script.php/fake_filename.doc

如果您的服务器支持URL重写(例如Apache中的mod_rewrite),则可以完全隐藏脚本部分。

URL中的字符应该是UTF-8编码,按字节进行url编码:

/mot%C3%B6rhead   # motörhead

3
尝试访问GetAttachment.aspx/fake_filename.doc?id=34(可能仅适用于Apache),但请注意不要改变原意。 - Kornel
6
我沿着这条莫名其妙的路径前进,尝试了其他解决方案;试图嗅探出正确的浏览器和版本以正确设置标头太过困难。Chrome错误地识别为Safari,导致行为完全不同(如果未正确编码,则会在逗号处中断)。省点心思吧,使用这个解决方案,并根据需要别名URL。 - mpen
3
“/:id/:filename” 这个方法非常简单且有效,谢谢! - Luca Steeb
2
这是一个很棒的解决方案(让我感到有点愚蠢)。另外,记住如果文件名来自用户变量,你仍然必须确保它已经准备好放入文件系统。如果不这样做,而文件名包含像 / 这样的字符,你会得到非常奇怪的浏览器错误。参考 这个答案,我使用了 s.replace(/[\000-\031\\\/:*?"<>\|]/g, '_') - Caleb Hearon
2
@GuneyOzsan 保存文件的文件名是由Web浏览器推断出来的,而浏览器对服务器端发生的情况没有任何理解,因此它们不理解也不关心服务器如何解释URL。浏览器只取URL路径中最后一个斜杠后面的内容,有时还会根据“Content-Type”尝试更正文件扩展名。 - Kornel
显示剩余13条评论

103

在建议的RFC 5987“超文本传输协议(HTTP)头字段参数的字符集和语言编码”中,讨论了这个问题,包括浏览器测试和向后兼容性的链接。

RFC 2183指出此类标头应根据RFC 2184进行编码,该规范已被RFC 2231取代,并由上述草案RFC进行覆盖。


5
请注意,互联网草案(不是“草案RFC”)已经完成,最终文件是RFC 5987(http://greenbytes.de/tech/webdav/rfc5987.html)。 - Julian Reschke
11
关于这个问题,我发现使用Firefox浏览器(版本4-9)下载文件时,如果文件名中含有逗号(,),如 Content-Disposition: filename="foo, bar.pdf",则会出现错误。具体表现为Firefox会正确下载文件,但文件名末尾会保留.part扩展名(例如foo,bar.pdf-1.part)。因此,该文件将无法正确打开,因为应用程序无法关联.part扩展名。其他ASCII字符似乎可以正常工作。 - catchdave
2
@MatthewSchinckel 例如 http://kbyanc.blogspot.hk/2010/07/serving-file-downloads-with-non-ascii.html 和 http://www.digiblog.de/2011/04/android-and-the-download-file-headers/ - Dennis C
3
有关IE行为的更多信息,请参见http://blogs.msdn.com/b/ieinternals/archive/2010/06/07/content-disposition-attachment-and-international-unicode-characters.aspx。 - EricLaw
5
你忘了加上“附件”的部分。 - Christoffer Hammarström
显示剩余6条评论

90

RFC 6266描述了“在超文本传输协议(HTTP)中使用内容-Disposition头字段”。引用自该文件:

6. 国际化考虑

filename*”参数(第4.3节),使用[RFC5987]中定义的编码,允许服务器传输ISO-8859-1字符集之外的字符,并可选择指定所使用的语言。

并在其示例部分中:

This example is the same as the one above, but adding the "filename" parameter for compatibility with user agents not implementing RFC 5987:

Content-Disposition: attachment;
                     filename="EURO rates";
                     filename*=utf-8''%e2%82%ac%20rates

Note: Those user agents that do not support the RFC 5987 encoding ignore “filename*” when it occurs after “filename”.

附录D中,还有一长串增加互操作性建议的列表。它还指向一个比较实现的站点。目前适用于常见文件名的全通测试包括:
  • attwithisofnplain:带双引号且不编码的纯ISO-8859-1文件名。这需要一个全部为ISO-8859-1且不含百分号(至少不在十六进制数字前面)的文件名。
  • attfnboth:按上述顺序使用两个参数。应该适用于大多数浏览器上的大多数文件名,尽管IE8将使用“filename”参数。
RFC 5987 又参照了 RFC 2231,后者描述了实际格式。2231 主要用于邮件,而 5987 告诉我们哪些部分也可用于 HTTP 标头。不要将此与 MIME 标头混淆,在 multipart/form-data HTTP body 中使用的标头受 RFC 2388(特别是 section 4.4)和 HTML 5 draft 管理。

1
我在Safari中遇到了问题。当下载具有俄语名称的文件时,会收到错误和无法读取的字符。解决方案已经帮助了我。但是我们需要在单行中发送一个标题(!!!)。 - evtuhovdo

15

请注意,可以提供文件名参数的两种编码方式,并且它们似乎在旧浏览器和新浏览器(在此情况下为MSIE8和Safari)中都能正常工作。请查看@AtifAziz提到的报告中的attfnboth - Pablo Montilla

14

7
很遗憾,这并不能解决上面答案中提到的所有问题。 - Luca Steeb
2
这将允许您返回带有空格、&%#等特殊字符的文件名。因此,它解决了这个问题。 - Don Cheadle
2
如果文件名包含双引号(是的,这可能会发生),根据RFC 6266规定,文件名是一个“引用字符串”,并且根据RFC 2616规定,在引用字符串中的双引号应该用反斜杠进行转义。 - Christophe Roussy
@ChristopheRoussy 有没有办法允许文件名中包含双引号?我尝试了很多用单引号包裹、转义双引号(\")等组合,但都不起作用。最终我只能使用 gsub 来删除双引号。所以如果 filenameMy 2" Report.doc,它最终会变成 My 2 Report.doc。虽然不是理想的解决方案,但至少它可以工作。你有什么想法吗? - Joshua Pinter
@JoshuaPinter,请查看转义或转义字符,有时您必须将字符加倍。它必须在标准中定义。关闭:https://dev59.com/M2Ml5IYBdhLWcg3wKkTW - Christophe Roussy
显示剩余2条评论

12

我使用以下代码片段进行编码(假设fileName包含文件名和扩展名,即:test.txt):


PHP:

if ( strpos ( $_SERVER [ 'HTTP_USER_AGENT' ], "MSIE" ) > 0 )
{
     header ( 'Content-Disposition: attachment; filename="' . rawurlencode ( $fileName ) . '"' );
}
else
{
     header( 'Content-Disposition: attachment; filename*=UTF-8\'\'' . rawurlencode ( $fileName ) );
}

Java:

Java:
fileName = request.getHeader ( "user-agent" ).contains ( "MSIE" ) ? URLEncoder.encode ( fileName, "utf-8") : MimeUtility.encodeWord ( fileName );
response.setHeader ( "Content-disposition", "attachment; filename=\"" + fileName + "\"");

1
在PHP中,至少对于filename*=的头部,应该使用rawurlencode,这是因为RFC 6266-> RFC 5987中的ext-value使用的value-chars(请参见http://tools.ietf.org/html/rfc6266#section-4.1和http://tools.ietf.org/html/rfc5987#section-3.2.1)不允许有空格而不进行百分号转义(但另一方面,`filename=`似乎可以完全不进行转义而允许有空格,尽管此处仅应包含ASCII)。没有必要以完全严格的方式进行编码,因此可以取消编码几个字符:https://gist.github.com/brettz9/8752120 - Brett Zamir

10
在ASP.NET Web API中,我对文件名进行URL编码:
public static class HttpRequestMessageExtensions
{
    public static HttpResponseMessage CreateFileResponse(this HttpRequestMessage request, byte[] data, string filename, string mediaType)
    {
        HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.OK);
        var stream = new MemoryStream(data);
        stream.Position = 0;

        response.Content = new StreamContent(stream);

        response.Content.Headers.ContentType = 
            new MediaTypeHeaderValue(mediaType);

        // URL-Encode filename
        // Fixes behavior in IE, that filenames with non US-ASCII characters
        // stay correct (not "_utf-8_.......=_=").
        var encodedFilename = HttpUtility.UrlEncode(filename, Encoding.UTF8);

        response.Content.Headers.ContentDisposition =
            new ContentDispositionHeaderValue("attachment") { FileName = encodedFilename };
        return response;
    }
}

IE 9 Not fixed
IE 9 Fixed


10

在asp.net mvc2中,我使用类似以下的代码:

return File(
    tempFile
    , "application/octet-stream"
    , HttpUtility.UrlPathEncode(fileName)
    );

我猜如果你不使用MVC(2),你可以只对文件名进行编码

HttpUtility.UrlPathEncode(fileName)

2
文件名编码的 URL 编码无效,浏览器不应该对其进行 URL 解码。 - SerialSeb
IE 11 绝对不会在此字段中解码 URL 编码。 - pseudocoder
但是当浏览器是Chrome或IE时,它需要进行UrlEncoded,其他浏览器如FF、Safari和Opera则可以正常工作而无需编码。 - Reza

9
在PHP中,以下代码适用于我(假设文件名为UTF8编码):
header('Content-Disposition: attachment;'
    . 'filename="' . addslashes(utf8_decode($filename)) . '";'
    . 'filename*=utf-8\'\'' . rawurlencode($filename));

已测试兼容IE8-11、Firefox和Chrome浏览器。
如果浏览器能够解释filename*=utf-8,它将使用UTF8版本的文件名;否则,它将使用解码后的文件名。如果你的文件名包含ISO-8859-1无法表示的字符,建议考虑使用iconv


3
尽管这段代码可能回答了问题,但提供关于为什么和/或如何回答问题的额外上下文将极大地提高其长期价值。请编辑您的答案以添加一些解释。 - Toby Speight
3
哇,以上这些仅由代码组成的答案都没有像那样被投票否决或批评。 而且我已经发现为什么已经有人回答了:IE无法解释filename*=utf-8,而需要使用文件名的ISO8859-1版本,而此脚本提供了这种情况的解决方式。 我只是想为PHP编写一个简单的可用代码,以方便懒惰的人使用。 - Gustav
我认为这个问题被downvote的原因是它不是针对某种特定编程语言,而是关于在实现header编码时应遵循哪些RFC的问题。但还是谢谢你的回答,对于PHP,这段代码解决了我的困扰。 - j4k3
1
谢谢。这个答案可能并没有严格回答问题,但它正是我所寻找的,并帮助我解决了Python中的问题。 - Lyndsy Simon
1
我非常确定,如果用户可以控制文件名,这段代码可以用作攻击向量。 - Antti Haapala -- Слава Україні
这在Safari中能用吗? - Flimm

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接