589

想要强制下载资源而不是直接在 Web 浏览器中呈现Content-Disposition的 Web 应用程序会在表单的 HTTP 响应中发出一个标头:

Content-Disposition: attachment; filename=FILENAME

filename参数可用于建议浏览器将资源下载到的文件的名称。然而, RFC 2183(Content-Disposition)在第 2.3 节(文件名参数)中声明文件名只能使用 US-ASCII 字符:

当前 [RFC 2045] 语法将参数值(以及因此的 Content-Disposition 文件名)限制为 US-ASCII。我们认识到允许在文件名中使用任意字符集是非常可取的,但是定义必要的机制超出了本文档的范围。

然而,有经验证据表明,当今大多数流行的 Web 浏览器似乎允许非 US-ASCII 字符(由于缺乏标准)在文件名的编码方案和字符集规范上存在分歧。那么问题来了,如果需要将文件名“naïvefile”(不带引号,第三个字母是 U+00EF)编码到 Content-Disposition 标头中,流行的浏览器采用的各种方案和编码是什么?

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

  • 谷歌浏览器
  • 苹果浏览器
  • Internet Explorer 或边缘
  • 火狐
  • 歌剧
4

21 回答 21

406

我知道这是一个旧帖子,但它仍然非常相关。我发现现代浏览器支持 rfc5987,它允许 utf-8 编码、百分比编码(url 编码)。然后 Naïve 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,所以我无法摆脱第一部分。事实证明,Android 上的浏览​​器使用内置的 Android 下载管理器,它无法以标准方式可靠地解析文件名。

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上所讨论的,它似乎不太好用。

于 2011-07-19T10:34:35.950 回答
178
  • 没有可互操作的方式在Content-Disposition. 浏览器兼容性一团糟

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

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

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

当最后一个斜杠后面的名称是您想要的名称时,您不需要任何额外的标题!

这个技巧有效:

/real_script.php/fake_filename.doc

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

URL 中的字符应为 UTF-8,按字节进行 urlencoded:

/mot%C3%B6rhead   # motörhead
于 2008-10-19T18:26:36.530 回答
102

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

RFC 2183表明此类标头应根据RFC 2184进行编码,该标准已被RFC 2231废弃,由上述 RFC 草案涵盖。

于 2008-09-18T15:39:58.607 回答
78

RFC 6266描述了“<em>在超文本传输​​协议 (HTTP) 中使用 Content-Disposition 标头字段”。从中引用:

6. 国际化注意事项

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

在他们的示例部分

此示例与上面的示例相同,但添加了“文件名”参数以与未实现 RFC 5987的用户代理兼容:

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

注意:那些不支持RFC 5987编码的用户代理会忽略出现在“<code>filename”之后的“<code>filename*”。

附录 D中还有一长串提高互操作性的建议。它还指向一个比较实现的站点。当前适用于常见文件名的全通测试包括:

  • attwithisofnplain:带双引号且不带编码的纯 ISO-8859-1 文件名。这需要一个全部为 ISO-8859-1 且不包含百分号的文件名,至少不包含在十六进制数字前面。
  • attfnboth:按上述顺序的两个参数。应该适用于大多数浏览器上的大多数文件名,尽管 IE8 将使用“<code>filename”参数。

RFC 5987又引用RFC 2231,它描述了实际格式。2231 主要用于邮件,5987 告诉我们哪些部分也可用于 HTTP 标头。不要将此与multipart/form-dataHTTP正文中使用的 MIME 标头混淆,后者受RFC 2388(特别是第 4.4 节)和HTML 5 草案的约束。

于 2014-01-05T12:48:27.443 回答
16

Jim在他的回答中提到的RFC 草案链接的以下文档进一步解决了这个问题,在这里绝对值得直接注意:

HTTP Content-Disposition 标头和 RFC 2231/2047 编码的测试用例

于 2008-09-18T16:08:16.900 回答
14

将文件名放在双引号中。为我解决了这个问题。像这样:

Content-Disposition: attachment; filename="My Report.doc"

http://kb.mozillazine.org/Filenames_with_spaces_are_truncated_upon_download

我已经测试了多个选项。浏览器不支持规范并且行为不同,我相信双引号是最好的选择。

于 2015-07-10T15:01:51.320 回答
13

我使用以下代码片段进行编码(假设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 ) );
}

爪哇:

fileName = request.getHeader ( "user-agent" ).contains ( "MSIE" ) ? URLEncoder.encode ( fileName, "utf-8") : MimeUtility.encodeWord ( fileName );
response.setHeader ( "Content-disposition", "attachment; filename=\"" + fileName + "\"");
于 2013-04-19T11:29:24.367 回答
11

在 asp.net mvc2 我使用这样的东西:

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

我想如果你不使用 mvc(2) 你可以只使用编码文件名

HttpUtility.UrlPathEncode(fileName)
于 2010-07-15T15:08:29.073 回答
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 未修复
IE 9 已修复

于 2015-06-25T08:10:39.727 回答
8

在 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改用。

于 2016-05-20T12:47:05.260 回答
6

如果您使用的是 nodejs 后端,则可以使用我在此处找到的以下代码

var fileName = 'my file(2).txt';
var header = "Content-Disposition: attachment; filename*=UTF-8''" 
             + encodeRFC5987ValueChars(fileName);

function encodeRFC5987ValueChars (str) {
    return encodeURIComponent(str).
        // Note that although RFC3986 reserves "!", RFC5987 does not,
        // so we do not need to escape it
        replace(/['()]/g, escape). // i.e., %27 %28 %29
        replace(/\*/g, '%2A').
            // The following are not required for percent-encoding per RFC5987, 
            // so we can allow for a little better readability over the wire: |`^
            replace(/%(?:7C|60|5E)/g, unescape);
}
于 2015-09-25T12:45:11.930 回答
5

我在所有主流浏览器中测试了以下代码,包括旧版 Explorer(通过兼容模式),它在任何地方都运行良好:

$filename = $_GET['file']; //this string from $_GET is already decoded
if (strstr($_SERVER['HTTP_USER_AGENT'],"MSIE"))
  $filename = rawurlencode($filename);
header('Content-Disposition: attachment; filename="'.$filename.'"');
于 2012-05-31T15:48:11.457 回答
5

我最终在我的“download.php”脚本中得到了以下代码(基于这篇博文和这些测试用例)。

$il1_filename = utf8_decode($filename);
$to_underscore = "\"\\#*;:|<>/?";
$safe_filename = strtr($il1_filename, $to_underscore, str_repeat("_", strlen($to_underscore)));

header("Content-Disposition: attachment; filename=\"$safe_filename\""
.( $safe_filename === $filename ? "" : "; filename*=UTF-8''".rawurlencode($filename) ));

只要仅使用 iso-latin1 和“安全”字符,这将使用标准的 filename="..." 方式;如果没有,它会添加 filename*=UTF-8'' url-encoded 方式。根据这个特定的测试用例,它应该从 MSIE9 开始工作,并在最近的 FF、Chrome、Safari 上工作;在较低的 MSIE 版本上,它应该提供包含 ISO8859-1 版本的文件名的文件名,并在不采用这种编码的字符上带有下划线。

最后说明:最大。在 apache 上,每个标头字段的大小为 8190 字节。UTF-8 每个字符最多可以有四个字节;在 rawurlencode 之后,每个字符 x3 = 12 个字节。效率很低,但理论上应该仍然可以在文件名中包含超过 600 个“微笑”%F0%9F%98%81。

于 2015-04-05T15:45:29.043 回答
5

只是一个更新,因为我今天尝试所有这些东西以响应客户问题

  • 除了为日语配置的 Safari 之外,我们客户测试的所有浏览器在使用 filename=text.pdf 时效果最佳 - 其中 text 是由 ASP.Net/IIS 在 utf-8 中序列化的客户值,没有 url 编码。出于某种原因,配置为英文的 Safari 会接受并正确保存具有 utf-8 日文名称的文件,但配置为日文的同一浏览器会使用未解释的 utf-8 字符保存文件。测试的所有其他浏览器似乎在文件名 utf-8 编码但没有 url 编码的情况下工作得最好/很好(无论语言配置如何)。
  • 我根本找不到一个实现 Rfc5987/8187浏览器。我使用最新的 Chrome、Firefox 构建以及 IE 11 和 Edge 进行了测试。我尝试仅使用 filename*=utf-8''texturlencoded.pdf 设置标题,同时使用 filename=text.pdf 设置它;文件名*=utf-8''texturlencoded.pdf。Rfc5987/8187 的任何一项功能似乎都没有在上述任何一项中得到正确处理。
于 2019-03-13T19:18:16.610 回答
3

PHP 框架 Symfony 4$filenameFallbackHeaderUtils::makeDisposition. 您可以查看此功能以获取详细信息 - 它类似于上面的答案。

使用示例:

$filenameFallback = preg_replace('#^.*\.#', md5($filename) . '.', $filename);
$disposition = $response->headers->makeDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT, $filename, $filenameFallback);
$response->headers->set('Content-Disposition', $disposition);
于 2019-07-22T13:58:45.870 回答
1

经典的 ASP 解决方案

大多数现代浏览器现在都支持传递,但Filename就像UTF-8我使用的基于FreeASPUpload.Net的文件上传解决方案一样 (站点不再存在,链接指向archive.org,它不能作为解析binary 依赖于读取单字节 ASCII 编码的字符串,当您传递 UTF-8 编码的数据直到您获得 ASCII 不支持的字符时,它工作得很好。

但是,我能够找到一种解决方案来获取将二进制文件读取和解析为 UTF-8 的代码。

Public Function BytesToString(bytes)    'UTF-8..
  Dim bslen
  Dim i, k , N 
  Dim b , count 
  Dim str

  bslen = LenB(bytes)
  str=""

  i = 0
  Do While i < bslen
    b = AscB(MidB(bytes,i+1,1))

    If (b And &HFC) = &HFC Then
      count = 6
      N = b And &H1
    ElseIf (b And &HF8) = &HF8 Then
      count = 5
      N = b And &H3
    ElseIf (b And &HF0) = &HF0 Then
      count = 4
      N = b And &H7
    ElseIf (b And &HE0) = &HE0 Then
      count = 3
      N = b And &HF
    ElseIf (b And &HC0) = &HC0 Then
      count = 2
      N = b And &H1F
    Else
      count = 1
      str = str & Chr(b)
    End If

    If i + count - 1 > bslen Then
      str = str&"?"
      Exit Do
    End If

    If count>1 then
      For k = 1 To count - 1
        b = AscB(MidB(bytes,i+k+1,1))
        N = N * &H40 + (b And &H3F)
      Next
      str = str & ChrW(N)
    End If
    i = i + count
  Loop

  BytesToString = str
End Function

通过在我自己的代码中实现该功能,我能够使文件名正常工作,这归功于Pure ASP File UploadBytesToString()include_aspuploader.aspUTF-8


有用的链接

于 2016-05-23T12:17:58.410 回答
1

从 .NET 4.5(和 Core 1.0)开始,您可以使用ContentDispositionHeaderValue为您进行格式化。

var fileName = "Naïve file.txt";
var h = new System.Net.Http.Headers.ContentDispositionHeaderValue("attachment");
h.FileNameStar = fileName;
h.FileName = "fallback-ascii-name.txt";

Response.Headers.Add("Content-Disposition", h.ToString());

h.ToString()将导致:

attachment; filename*=utf-8''Na%C3%AFve%20file.txt; filename=fallback-ascii-name.txt
于 2021-07-30T07:34:31.133 回答
0

对于那些需要 JavaScript 编码标头的方法的人,我发现这个函数运行良好:

function createContentDispositionHeader(filename:string) {
    const encoded = encodeURIComponent(filename);
    return `attachment; filename*=UTF-8''${encoded}; filename="${encoded}"`;
}

这是基于 Nextcloud 在下载文件时似乎正在做的事情。文件名首先以 UTF-8 编码出现,并且可能为了与某些浏览器兼容,文件名也出现时不带 UTF-8 前缀。

于 2021-08-17T22:30:43.460 回答
0

库类 Unicode 中的 mimeHeaderEncode($string) 方法可以完成这项工作。

drupal/php 中的示例:

https://github.com/drupal/core-utility/blob/8.8.x/Unicode.php

/**
   * Encodes MIME/HTTP headers that contain incorrectly encoded characters.
   *
   * For example, Unicode::mimeHeaderEncode('tést.txt') returns
   * "=?UTF-8?B?dMOpc3QudHh0?=".
   *
   * See http://www.rfc-editor.org/rfc/rfc2047.txt for more information.
   *
   * Notes:
   * - Only encode strings that contain non-ASCII characters.
   * - We progressively cut-off a chunk with self::truncateBytes(). This ensures
   *   each chunk starts and ends on a character boundary.
   * - Using \n as the chunk separator may cause problems on some systems and
   *   may have to be changed to \r\n or \r.
   *
   * @param string $string
   *   The header to encode.
   * @param bool $shorten
   *   If TRUE, only return the first chunk of a multi-chunk encoded string.
   *
   * @return string
   *   The mime-encoded header.
   */
  public static function mimeHeaderEncode($string, $shorten = FALSE) {
    if (preg_match('/[^\x20-\x7E]/', $string)) {
      // floor((75 - strlen("=?UTF-8?B??=")) * 0.75);
      $chunk_size = 47;
      $len = strlen($string);
      $output = '';
      while ($len > 0) {
        $chunk = static::truncateBytes($string, $chunk_size);
        $output .= ' =?UTF-8?B?' . base64_encode($chunk) . "?=\n";
        if ($shorten) {
          break;
        }
        $c = strlen($chunk);
        $string = substr($string, $c);
        $len -= $c;
      }
      return trim($output);
    }
    return $string;
  }
于 2021-12-21T10:49:51.597 回答
-1

我们在 Web 应用程序中遇到了类似的问题,最终通过从 HTML 读取文件名<input type="file">,并将其设置为新 HTML 中的 url 编码形式<input type="hidden">。当然,我们必须删除某些浏览器返回的路径,例如“C:\fakepath\”。

当然,这并不能直接回答 OP 的问题,但可能是其他人的解决方案。

于 2015-01-27T11:54:13.313 回答
-2

我通常对文件名进行 URL 编码(使用 %xx),它似乎适用于所有浏览器。无论如何,您可能都想做一些测试。

于 2008-09-18T15:28:29.493 回答