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

.NET MVC下使用SMTP Server发送大附件邮件触发ObjectDisposedException问题

.NET MVC 上传附件发邮件触发 ObjectDisposedException 问题排查

问题现象

  • 生产环境通过内部SMTP服务器发送带附件的邮件时,附件总大小超过150KB即触发运行错误
  • 错误触发与SMTP服务端配置无关:服务端明确配置了单封邮件附件大小上限为25MB,远高于报错触发阈值
  • 本地开发环境测试功能完全正常,无报错、无附件读取异常
  • 业务代码未显式声明使用FileStream类操作上传文件
  • 故障表现为邮件发送流程中断、生成的附件资源无法正常打开访问

运行时异常堆栈

异常类型: System.ObjectDisposedException
异常信息: Cannot access a closed file.
异常来源: mscorlib
   at System.IO.__Error.FileNotOpen()
   at System.IO.FileStream.Seek(Int64 offset, SeekOrigin origin)
   at System.Web.HttpRawUploadedContent.TempFile.GetBytes(Int32 offset, Int32 length, Byte[] buffer, Int32 bufferOffset)
   at System.Web.HttpRawUploadedContent.CopyBytes(Int32 offset, Byte[] buffer, Int32 bufferOffset, Int32 length)
   at System.Web.HttpInputStream.Read(Byte[] buffer, Int32 offset, Int32 count)
   at System.Net.Mime.MimePart.Send(BaseWriter writer, Boolean allowUnicode)
   at System.Net.Mime.MimeMultiPart.Send(BaseWriter writer, Boolean allowUnicode)
   at System.Net.Mail.SmtpClient.Send(MailMessage message)

问题根因

异常触发链路和ASP.NET的上传文件处理机制直接相关:

  • 用户通过页面上传文件时,ASP.NET会根据上传内容大小自动选择存储位置:小于配置阈值的内容直接存在内存中,超过阈值的内容会自动写入服务器临时目录,对应的临时文件操作流由框架自动创建(对应堆栈中HttpRawUploadedContent.TempFile依赖的FileStream),这也是业务代码没有显式编写FileStream逻辑,却能在堆栈中看到相关调用的原因。
  • 原有实现直接把上传文件自带的file.InputStream传给Attachment构造函数,这个输入流的生命周期和当前HTTP请求强绑定:当SmtpClient.Send()执行、需要读取附件内容生成MIME格式邮件时,如果附件因大小超过阈值走了临时磁盘存储路径,对应的临时流会被ASP.NET请求管线提前回收关闭,此时SMTP客户端调用流的Seek/Read方法就会抛出“无法访问已关闭的文件”的异常。
  • 本地测试无问题、生产环境超150KB才报错的差异来自环境配置:本地与生产环境的ASP.NET上传内容内存缓存阈值不同,150KB刚好卡在生产环境的阈值分界点——小于阈值时附件内容存在内存中,流不会被提前回收;超过阈值时附件转为临时磁盘存储,流被提前关闭触发报错。整个错误发生在应用本地准备邮件内容的阶段,未走到与SMTP服务器通信的步骤,因此SMTP端的25MB附件配置不会对这个错误产生任何影响。

修复方案

修改附件绑定逻辑,将上传文件内容拷贝到与HTTP请求生命周期解耦的独立流中,再传给Attachment对象,避免原上传流被提前回收。
核心修改代码示例:

// 在using作用域内创建MailMessage,便于自动释放关联资源
using (MailMessage mail = new MailMessage())
{
    mail.From = new MailAddress(model.from);
    mail.To.Add(new MailAddress(model.to));
    mail.Subject = model.subject;
    mail.Body = model.body;
    mail.IsBodyHtml = true;  

    foreach (var file in model.attachments)
    {
        // 创建独立内存流存储附件内容,脱离HTTP请求生命周期绑定
        using var attachmentStream = new MemoryStream();
        // 将原上传流位置重置到起始点,避免读取偏移导致内容丢失
        file.InputStream.Position = 0;
        // 将上传内容全量拷贝到独立流
        await file.InputStream.CopyToAsync(attachmentStream);
        // 重置独立流位置到开头,供SmtpClient读取内容
        attachmentStream.Position = 0;
        // 将独立流传给Attachment,原流的生命周期变化不会影响附件读取
        mail.Attachments.Add(new Attachment(attachmentStream, file.FileName));
    }

    smtpClient.Send(mail);
}

修复注意点

  • 拷贝流之前必须将原上传流的Position属性重置为0,否则会从上次读取的位置开始拷贝,导致附件内容残缺无法打开
  • 大附件场景如果担心内存占用过高,可以改用临时文件流存储附件内容,只要保证流的生命周期覆盖整个邮件发送流程即可
  • 邮件发送完成后及时释放MailMessage和附属的流资源,避免内存泄漏

内容的提问来源于stack exchange,提问作者dave_26456

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.24 18:15:04