.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
相关产品推荐
相关产品推荐

