Chrome与Edge浏览器中含撇号的UTF-8附件文件名下载异常问题
问题描述
Chrome和Edge浏览器在处理包含撇号的UTF-8格式Content-Disposition响应头时,会出现下载文件名错误的问题。例如请求URL https://localhost:44328/Home/GetDocument?id=2,服务端返回响应头:
content-disposition: attachment; filename*=UTF-8''Supplier's Notes.txt
Firefox能正常识别并下载为Supplier's Notes.txt,但Chrome和Edge会将文件名识别为GetDocument.htm。
原因分析
这是Chrome和Edge对RFC 5987标准(定义HTTP头中UTF-8编码参数格式)的严格解析导致的。根据标准,filename*参数值中的特殊字符(包括撇号')需要进行百分号编码(%27)。Firefox对未编码的特殊字符做了兼容处理,但Chrome/Edge严格遵循标准,解析失败后会 fallback 使用请求URL的最后一段作为默认文件名。
解决方法
方法1:对文件名中的特殊字符进行百分号编码
构造filename*参数时,对文件名中的非安全字符(如撇号、空格等)进行URL编码,确保符合RFC 5987标准。
修改后的HomeController.cs中GetDocument方法代码:
public void GetDocument(int id) { if (!SampleViewModel.SampleData.ContainsKey(id)) { return; } var fileName = SampleViewModel.SampleData[id]; // 对文件名进行URL编码,符合RFC 5987要求 var encodedFileName = System.Web.HttpUtility.UrlPathEncode(fileName); var attachmentString = $"filename*=UTF-8''{encodedFileName}"; this.HttpContext.Response.Clear(); this.HttpContext.Response.AddHeader("Content-Disposition", $"attachment; {attachmentString}"); this.HttpContext.Response.AddHeader("Content-Type", "application/octet-stream"); // 修正原代码拼写错误 var bytes = Encoding.ASCII.GetBytes("Hello World"); this.HttpContext.Response.OutputStream.Write(bytes, 0, bytes.Length); this.HttpContext.Response.Flush(); this.HttpContext.Response.End(); }
方法2:同时提供传统filename参数作为兼容方案
除了UTF-8格式的filename*,可以同时添加传统的filename参数(转义特殊字符),作为严格解析浏览器的 fallback:
var fileName = SampleViewModel.SampleData[id]; var encodedFileName = System.Web.HttpUtility.UrlPathEncode(fileName); // 转义传统filename参数中的撇号 var legacyFileName = fileName.Replace("'", "\\'"); var attachmentString = $"attachment; filename=\"{legacyFileName}\"; filename*=UTF-8''{encodedFileName}";
可复现示例代码(原代码)
HomeController.cs
using DownloadNameWithApostrophe.Models; using System.Text; using System.Web.Mvc; namespace DownloadNameWithApostrophe.Controllers { public class HomeController : Controller { public ActionResult Index() { return View(new SampleViewModel()); } public void GetDocument(int id) { if (!SampleViewModel.SampleData.ContainsKey(id)) { return; } var attachmentString = "filename*=UTF-8''" + SampleViewModel.SampleData[id]; this.HttpContext.Response.Clear(); this.HttpContext.Response.AddHeader("Content-Disposition", "attachment; " + attachmentString); this.HttpContext.Response.AddHeader("Contenty-type", "application/octet-stream"); var bytes = Encoding.ASCII.GetBytes("Hello World"); this.HttpContext.Response.OutputStream.Write(bytes, 0, bytes.Length); this.HttpContext.Response.Flush(); this.HttpContext.Response.End(); } } }
SampleViewModel.cs
using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; namespace DownloadNameWithApostrophe.Models { public class SampleViewModel { public static Dictionary<int, string> SampleData { get { var downloads = new Dictionary<int, string>(); downloads.Add(1, "Initial Estimate.txt"); downloads.Add(2, "Supplier's Notes.txt"); return downloads; } } [Display(Name = "Available Downloads")] public Dictionary<int, string> Downloads { get; set; } public SampleViewModel() { Downloads = SampleData; } } }
Index.cshtml
@model DownloadNameWithApostrophe.Models.SampleViewModel @{ Layout = null; } <div class="row"> <div class="col-md-4"> <h2>Getting started</h2> @Html.LabelFor(m => m.Downloads) @foreach (var download in Model.Downloads) { <br /> <a href="@Url.RouteUrl(new {action="GetDocument", controller="Home"})?id=@download.Key">@download.Value</a> } </div> </div>
内容的提问来源于stack exchange,提问作者Sheridan
相关产品推荐
相关产品推荐

