ASP.NET MVC下载文件时fileContents转字符串、丢失文件名问题
问题根因
- 用
return Json()包裹FileContentResult返回时,ASP.NET MVC默认序列化逻辑会自动把byte[]类型的fileContents转成Base64字符串,你看到的无意义字符串就是Base64编码结果,不是乱码,只是没有做对应解码就直接传入Blob构造函数,导致生成的文件损坏。 - 直接返回FileResult时,
$.ajax默认以纯文本格式解析响应体,不会主动读取响应头,而FileDownloadName属性的值最终是被框架写入Content-Disposition响应头的,自然拿不到正确文件名。
方案1:直接返回FileResult + 前端Blob解析(推荐,无额外编解码开销)
这个方案性能最好,适合绝大多数文件下载场景,没有多余的序列化损耗。
后端调整
去掉外层的return Json()包装,直接返回构造好的FileContentResult即可,原有文件初始化逻辑不需要改动:
var fileToDownload = new FileContentResult(fileBytes, System.Net.Mime.MediaTypeNames.Application.Octet) { // 文件名含中文/特殊字符时先做URL编码,避免前端读取乱码 FileDownloadName = Uri.EscapeDataString(fileNameWithExtension) }; return fileToDownload;
前端调整
给ajax请求添加响应类型配置,强制把响应当成Blob处理,不要让jQuery默认按文本解析,同时从响应头中提取文件名:
$.ajax({ type: 'POST', url: 'mydownloadurl', data: uploadFileFormData, cache: false, contentType: false, processData: false, // 关键配置:指定响应类型为blob xhrFields: { responseType: 'blob' }, success: function (data, status, xhr) { // 从Content-Disposition头解析文件名 const disposition = xhr.getResponseHeader('Content-Disposition'); let fileName = '未命名文件'; if (disposition && disposition.includes('attachment')) { const filenameMatch = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/.exec(disposition); if (filenameMatch?.[1]) { fileName = filenameMatch[1].replace(/['"]/g, ''); // 对应后端的URL编码,这里做解码 fileName = decodeURIComponent(fileName); } } // 此时拿到的data本身就是合法Blob对象,不需要重复构造 const blob = data; const isIE = !!document.documentMode; if (isIE) { window.navigator.msSaveBlob(blob, fileName); } else { const objectUrl = (window.URL || window.webkitURL).createObjectURL(blob); const a = document.createElement('a'); a.download = fileName; a.href = objectUrl; document.body.append(a); a.click(); document.body.removeChild(a); // 释放内存,避免内存泄漏 URL.revokeObjectURL(objectUrl); } }, error: function (xhr) { console.log('下载失败', xhr.status); } })
如果用axios实现,逻辑完全一致,只需要在请求配置里加responseType: 'blob',再从响应头res.headers['content-disposition']中解析文件名即可。
方案2:JSON结构显式返回文件内容和文件名(适合需要同时返回业务参数的场景)
如果你的接口除了文件还要返回业务状态码、提示信息等内容,必须用JSON格式返回的话,不要直接序列化FileContentResult对象,手动构造返回结构即可。
后端调整
手动将字节数组转为Base64字符串,和文件名、业务状态一起打包返回:
return Json(new { success = true, msg = "文件生成成功", fileBase64 = Convert.ToBase64String(fileBytes), fileName = fileNameWithExtension }, JsonRequestBehavior.AllowGet);
前端调整
拿到JSON响应后,将Base64字符串解码为二进制数组,再构造Blob触发下载:
$.ajax({ type: 'POST', url: 'mydownloadurl', data: uploadFileFormData, cache: false, contentType: false, processData: false, dataType: 'json', success: function (res) { if (!res.success) { alert(res.msg || '下载失败'); return; } // Base64转二进制字节数组 const binaryStr = atob(res.fileBase64); const byteArr = new Uint8Array(binaryStr.length); for (let i = 0; i < binaryStr.length; i++) { byteArr[i] = binaryStr.charCodeAt(i); } const blob = new Blob([byteArr], { type: 'application/octet-stream' }); const fileName = res.fileName; // 下载逻辑和方案1一致 const isIE = !!document.documentMode; if (isIE) { window.navigator.msSaveBlob(blob, fileName); } else { const objectUrl = (window.URL || window.webkitURL).createObjectURL(blob); const a = document.createElement('a'); a.download = fileName; a.href = objectUrl; document.body.append(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(objectUrl); } }, error: function (xhr) { console.log('请求失败', xhr.status); } })
注意:方案2存在Base64编解码开销,文件体积超过100M时性能会明显下降,大文件下载优先选择方案1。
内容的提问来源于stack exchange,提问作者CodingGoneWrong
相关产品推荐
相关产品推荐

