You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.09 15:25:19