
前几天群里有人发了个截图上传一个近 800MB 的文件界面转了半分钟然后跳出一条“404 - Not Found”。他满脸问号我一眼就看出这不是代码 bug这是 IIS 在背后把请求拦了。做 C# 服务端的人迟早都会撞上这一堵墙——IIS 对上传文件大小有一套默认的限制你不在配置层显式放开就算 C# 代码写得花团锦簇也白搭。这篇内容就是给所有用 C# 开发、把 Web 应用部署在 IIS 上、又被迫处理大文件上传的人。我会从最常见的报错症状讲起把 IIS 和 ASP.NET 两层管理机制拆开说一遍然后给出可以直接复制到生产环境的配置写法再附上一段 C# 流式处理上传的实际代码。看完你不仅能配置成功还能明白为什么以前改了 Web.config 却仍然没用以及后续性能调优的几个方向。1. 大文件上传失败的现场症状、日志与最初的那一记闷棍1.1 典型的失败场景复盘先回忆一下最常见的几个现象。上传文件在几十兆以下时一切正常一旦超过某个阈值比如说在 28MB、30MB 或者 100MB 左右请求就会当场失败。浏览器控制台看到的状态码五花八门404、413、400偶尔还有 500。更迷惑人的是有些人这边是 404那边换台电脑却报 413于是第一反应就是“代码没问题是网络问题”然后陷入无休止的日志翻找。其实这些状态码背后对应的是不同的限制层级。我之前做过一个模拟项目 X当时需要支持用户上传 200MB 的素材包刚上线测试时 100MB 的文件直接超时查日志才发现请求根本没进业务代码全被 IIS 前置管道拦掉了。那种感觉就像你把信投进邮筒结果邮筒盖子锁着你还在怪邮局送得慢。这类问题最冤的地方在于开发环境常常不走 IIS而是用开发服务器直接跑所以电脑上怎么传都没事。一部署到服务器立刻现原形。很多新手会在 Web.config 里加一个maxRequestLength发现没用于是来问我“到底要配哪里”。1.2 从日志中定位问题404.13与500.0的区分当 IIS 拒绝一个请求时它会返回一个附带子状态码的错误页面。比如404.13 - Content Length Too Large请求体长度超过 IIS 请求过滤模块的限制。413.1 - Request Entity Too Large同样与请求内容长度有关但可能发生在 Web.config 限制上。500.0 - Module DLL could not be loaded这种往往是 ASP.NET 运行时抛出的超额异常比如MaxRequestLength已经超限。日志文件是破案的关键。别只盯着浏览器看你要去服务器的 Windows 日志里翻。如果启用了 IIS 的错误日志打开C:\inetpub\logs\FailedReqLogFiles之类的路径能看到被拦截的具体原因。没有启用的话直接看系统事件查看器里的 ASP.NET 错误里面通常会写“Request Entity Too Large”或“Maximum request length exceeded”。我习惯的做法是先分清楚错误发生在哪一层。如果是 404.13那是 IIS 的 Http.sys 或请求过滤模块直接挡在外面跟 C# 代码毫无关系。如果是 500 且日志里有System.Web.HttpException: Maximum request length exceeded那才是 ASP.NET 运行时不让你的代码拿到完整请求体。这两者的处理方式不同所以宁可多花一分钟查日志也不要靠猜。2. 两道关卡IIS请求过滤与ASP.NET运行时谁掐死了你的大文件2.1 IIS层的maxAllowedContentLengthIIS 在向内传递请求之前会先经过一个叫 Request Filtering 的组件。这个组件从 IIS 7 开始就一直存在它默认给请求内容长度Content-Length设了一个上限30MB也就是 30000000 字节。这个值是写死在服务器级别的applicationHost.config里的你可能根本不知道它的存在。为什么 IIS 要干这件事安全考虑。服务器不可能允许客户端随便发一个数 GB 的流过来把内存和磁盘打满。所以它在最外层设了一道闸门超过 30MB 的请求体连你的 Web 应用都见不到就被丢掉了。要改这个值不能在站点级别直接改更不能只改 Web.config。你得去改C:\Windows\System32\inetsrv\config\applicationHost.config或者在 IIS 管理器的“请求过滤”功能里调整“编辑功能设置”中的“内容长度上限Bytes”。这个值是字节单位如果你要放行 1GB就是 1073741824。2.2 ASP.NET层的maxRequestLength与executionTimeout过了 IIS 这一关请求会交给 ASP.NET 运行时这里说的是经典模式或集成模式下托管 CLR 的情况。ASP.NET 有自己的一套限额主要是httpRuntime节点的两个属性maxRequestLength以 KB 为单位默认值是 4096 KB也就是 4MB。这个 4MB 比 IIS 的 30MB 还狠所以有的人改了 IIS 那一层之后仍然失败就是因为这一层没改。executionTimeout以秒为单位默认 110 秒。大文件上传时如果网络带宽不足传输时间超过了这个值请求同样会被终止报超时错误。这里的处理逻辑是IIS 是“我不让请求进内网”ASP.NET 是“你的请求已经进来但我规定你最长只能长这样多出的字节我不读”。所以两道卡口必须同时放行否则不是死在前门就是死在二门。2.3 我见过的一次典型双重拦截案例记得有一次帮某企业排查问题客户说上传 50MB 的视频老是报错。我登上去一看IIS 请求过滤上限是 30MB按理说 50MB 过不了这一关报 404.13 很合理。可是客户说他们已经在 Web.config 里配了httpRuntime maxRequestLength“81920”。这就尴尬了因为 Web.config 的这一项只管 ASP.NET 层但 IIS 那层的 30MB 还是原值。最终我把两层全部改成 200MB 后文件顺利上传。这件事给我的教训是配置不是随便加几行就好你要清楚自己改的是哪一个层的哪个参数。后来我再排查上传相关的问题都是先看applicationHost.config再看项目的Web.config两边对齐才收工。3. 手动配置实战applicationHost.config、aspnet.config与Web.config的正确改法3.1 修改IIS请求过滤设置我有两种改法看你是喜欢图形界面还是命令行。生产服务器上我通常直接用文本编辑器改配置文件因为有时候远程桌面反应慢开一次 IIS 管理器能卡半天。方法一IIS 管理器操作打开 IIS 管理器选中你的站点双击“请求过滤”然后在右侧点击“编辑功能设置”把“内容长度上限Bytes”改成你要的字节数。这里注意这个设置是站点级别的会写到站点对应的web.config里但它其实是 Microsoft IIS 配置节的映射最终生效时还是走请求过滤模块。改完之后不需要重启 IIS但最好回收一下站点确保干净。方法二直接修改 applicationHost.config用管理员权限打开记事本或者用 VS Code找到configuration/system.webServer/security/requestFiltering节点如果没有requestLimits子节点就加一个像这样system.webServer security requestFiltering requestLimits maxAllowedContentLength1073741824 / /requestFiltering /security /system.webServermaxAllowedContentLength单位是字节1073741824就是 1GB。如果你不想设这么大按实际业务来。有些场景下根本没必要放行 1GB比如只允许图片上传那 20MB 就够了。改这个文件之前一定要先备份。别问我为什么强调我手误删过节点导致整站 500那天差点熬夜加班。3.2 修改ASP.NET运行时设置接下来是Web.config里的httpRuntime配置。这个文件就在你的项目根目录可以直接在 Visual Studio 里面改。比如要放行到 1GB 并且把超时时间拉到 30 分钟可以这样写system.web httpRuntime maxRequestLength1048576 executionTimeout1800 / /system.web注意maxRequestLength的单位是 KB1048576KB 正好是 1GB。executionTimeout单位是秒1800 就是 30 分钟。这里有一个很坑的地方executionTimeout只对Page管束严格对异步处理程序比如IHttpAsyncHandler不一定起作用所以你在写 C# 上传接口时最好用异步方式否则你的长耗时任务会被这个超时砍掉。另外还有一个容易忽略的点ASP.NET 4.6.2 之后httpRuntime里多了一个maxRequestLength的升级版行为配合集成模式使用时HTTP.sys 想要直接流式处理大请求还会受maxAllowedContentLength制约。这就是为什么我前面说两个层要一起改不能只盯一个。3.3 配置文件修改后的重启与验证改配置文件不是改完就能立刻生效的。applicationHost.config的更改通常会触发 IIS 自动检测但最好运行一下iisreset /restart或者至少在你的站点上执行“回收应用程序池”。注意iisreset会重启整个 IIS可能会中断别的站点如果有多站点共享这台服务器要提前沟通。Web.config的改动一般是自动生效的但如果你动的是applicationHost.config建议做完之后用浏览器实际传一个比原来限制更大的文件或者用命令行测试。我个人喜欢用curl来做验证以下命令可以直接发送一个上传文件并输出响应头curl -k -X POST -F fileD:\test\largefile.zip https://localhost/upload -v-k是为了跳过证书验证-F是表单上传。如果原来的 30MB 限制被你改成 1GB那一个 500MB 的文件应该能拿到正常的响应码而不是 404 或 413。4. C#端的关键实践从HttpPostedFile到流式写入的升级之路4.1 传统上传代码为什么容易内存溢出很多老项目处理上传用的都是HttpPostedFileBase或者HttpFileCollection直接file.SaveAs(path)。这种写法对于小文件很舒服但一旦文件上了几百兆一个显而易见的隐患就出来了整个请求体都会被读入工作进程的内存连同文件内容和多部分表单的其他字段一起。在 32 位应用程序池里单个进程内存通常只有 4GB两个 1GB 文件并发上来你的站点直接因为OutOfMemoryException崩溃。另一个问题是这种方式没法提供进度反馈用户看着一个静止的界面等很久体验极差。所以做高端一点的 C# 上传我会选择流式方式不把整个请求体缓存起来而是边接收边写入磁盘。4.2 一个可直接运行的流式上传处理类流式上传的核心思路是用 ASP.NET 的Request.GetBufferlessInput()方法它允许你在不加载整个请求体的情况下读取输入流。下面这个简化版示例接收multipart/form-data格式的数据解析出字节流并写文件。实际生产环境里你还需要处理文件名、边界字符串等细节这里展示骨架思路using System; using System.IO; using System.Text; using System.Web; public class StreamingUploadHandler : IHttpHandler { public bool IsReusable false; public void ProcessRequest(HttpContext context) { string saveFolder D:\Uploads; if (!Directory.Exists(saveFolder)) Directory.CreateDirectory(saveFolder); // 读取原始的 HTTP 输入流而不是 HttpRequest.Files using (Stream input context.Request.GetBufferlessInputStream()) { // 这里因为你已经知道 requestLimits 放行了所以可以放心读取 byte[] buffer new byte[81920]; string outputPath Path.Combine(saveFolder, stream_ DateTime.Now.Ticks .dat); using (FileStream output new FileStream(outputPath, FileMode.Create, FileAccess.Write)) { int bytesRead; while ((bytesRead input.Read(buffer, 0, buffer.Length)) 0) { output.Write(buffer, 0, bytesRead); } } } context.Response.ContentType text/plain; context.Response.Write(upload success); } }这段代码故意只做原始字节流保存不做multipart解析因为真实的 multipart 格式需要你自己处理边界分隔符。实际项目中我更推荐用现成的库比如从 NuGet 找一些专门做流式 multipart 解析的组件或者直接改造开源代码。你要是感兴趣我可以后续再写一篇专门讲 multipart 边界解析的实现。使用GetBufferlessInputStream的一个大坑是你不能同时访问Request.Files和Request.Form因为那些集合已经触发过请求体读取再读输入流就空白了。解决办法是在读取流时自己解析或者干脆自定义一个前端上传组件只发送原始二进制服务端以流的方式接住。4.3 前端配合设置超时与进度反馈C# 和 IIS 配置搞定之后前端也得跟上。你用 XMLHttpRequestfetch 也行上传时默认没有超时概念但如果网络慢代理层可能先断开连接。我在做某跨平台系统的时候用过XMLHttpRequest的upload.onprogress事件来显示进度同时把timeout设置成和服务器侧一致const xhr new XMLHttpRequest(); xhr.open(POST, /upload/stream, true); xhr.timeout 30 * 60 * 1000; // 30分钟 xhr.upload.onprogress (e) { if (e.lengthComputable) { const percent Math.round((e.loaded / e.total) * 100); console.log(已上传${percent}%); } }; xhr.onload () { // 处理返回结果 }; xhr.send(file);这里timeout设成 30 分钟是和服务端executionTimeout对齐的。别小看这个细节我以前有一次服务端改成 30 分钟前端默认 0就是不超时结果浏览器和 IIS 之间保持的 TCP 连接在长时间空闲时被某些中间设备断开上传到 80% 后直接断掉而服务端已经写了一半的文件产生一个不完整的半成品文件。5. 配置完成后的验证方法与性能实测5.1 用curl和浏览器做边界测试配置改完不要急着让用户点“上传”。我会先在服务器本机跑一轮边界测试。比如上传 20MB 文件看是否正常响应。上传比原限制略大一点的比如 31MB确认不再返回 404.13。上传一个刚好 1GB 的文件看稳定性和耗时。有些浏览器自带开发工具能看到上传进度但如果你要精确知道服务器返回了什么状态码curl -v是最直观的。还有一种思路在服务器上临时放一个各种大小的测试文件目录写脚本循环调用上传接口把每个文件的状态码打出来。脚本不用复杂PowerShell、Python 都行我常用的是 Python 脚本。5.2 并发上传与网络中断的实测体验单文件上传成功不代表生产环境就稳了。大文件往往意味着大流量你要么在公司内部网络传输要么让用户从公网拖文件。我在模拟项目 X 里测试过同一时刻三个用户各传一个 200MB 文件服务器 CPU 和内存都还能扛住但是磁盘写入成了瓶颈。如果用的是机械硬盘随机写入速度会被并发拉低有时候一个请求的耗时比预期多出一倍。另外要测网络中断。上传到一半把网线拔了IIS 端连接会进入重试或直接断开。此时服务端代码如果能做好“断点续传”就能节省大量带宽。不过这是另一个主题了至少你要知道不是配置放行就万事大吉服务端要主动处理异常和半成品清理。5.3 基于个人经验的上传大小建议与折中方案在生产环境我通常不建议真的放行 2GB 甚至更大。原因很简单越大越难调试、越容易被攻击利用。我们公司做过一个素材库系统最后实际放行是 500MB再大的文件走后台专用通道或者要求用户压缩分包上传。这是因为 HTTP 上传对公网带宽和链路质量要求都很高一个 2GB 的文件在普通宽带下传一小时中间跨城市丢包的概率太高用户体验很差。更好的折中方案是分片上传。客户端把文件切成 5MB 左右的块逐块发送服务端校验块哈希最后合并。这样任何一个块失败只需要重传那个块而不是整个文件。分片上传的前端实现和 C# 端接口比流式上传复杂一些但它在生产环境的价值远高于单纯调大配置文件。6. 配置放行之后还有三个容易踩的坑6.1 修改错配置文件层级有人改applicationHost.config时把节点放到了location path下面结果变成对所有站点生效。有时候这是你要的但很多时候你只想改某一个站点这就需要在location中指定站点路径比如pathMySite。如果不小心放错了层级所有站点都会被放开限制你等于把一个安全防线给拆了。我之前在一个客户那里看到过这种配置他当时还没意识到安全隐患我帮他缩小到了指定站点。6.2 忽略代理服务器或负载均衡器的限制如果你前面还有一台 nginx 或负载均衡设备那么 IIS 配置得再好也可能没用。很多反向代理默认对请求体也有大小限制例如 nginx 的client_max_body_size默认 1MB。这时候你需要去改代理层。我在某跨平台系统里就曾因为只改了 IIS 而忽略了一层 nginx导致前后端测试出现非常不一致的结果。排查这种事情一定要画一遍请求链路图浏览器 → 反向代理 → IIS → ASP.NET。每一层的限制你都要检查。6.3 伪装成404的413响应体被截断的排查最后一个坑很有迷惑性。大文件上传时如果你的服务端逻辑枚举了Request.Files并且保存失败你可能会自定义返回一个 JSON 错误。但 IIS 层一旦先拒绝它返回的 404 错误页面包含了“Content-Length Too Large”之类的 HTML 描述。浏览器会尝试读取这个错误页但错误页的内容本身可能被截断屏幕上只显示一个惨白的 404根本没有具体信息。遇到这种情况别凭肉眼看浏览器。用curl -v或者 F12 看响应头特别是X-ASPNET-Version和Server字段能帮你判断这个 404 是从哪一层返回的。如果Server字段是Microsoft-HTTPAPI/2.0那是 HTTP.sys 直接回的跟你的 C# 代码毫无关系。最后再分享一个我坚持了好几年的习惯每次调整完这类配置都会把旧的配置文件和新的配置文件做一个 diff然后贴到团队的发布文档里。不是为了证明我做了什么而是为了下一次别人遇到“为什么以前能传现在不能传”的时候能快速定位到是哪个配置变了。大文件上传这件事配置看起来就那么几行但藏在背后的链路、超时、代理、内存、磁盘并发每一项都值得你在上线前一天提前验证。