HTTP模块捕获图片为字节数组后重显失败:图片损坏排查
图片HTTP缓存模块导致图片损坏的问题分析
问题场景
我编写了一个实现图片响应缓存的HTTP模块,使用静态字典存储图片二进制数据:
public static class ImageCacheManager { public static Dictionary<string, byte[]> TheImageResponseCache = new Dictionary<string, byte[]>(); }
模块运行无报错,但缓存后重放的图片无法显示,Chrome控制台显示请求正常但图片数据已损坏。完整模块代码如下:
public class ModuleImageCache : IHttpModule { public void Init(HttpApplication context) { context.BeginRequest += OnBeginRequest; context.EndRequest += OnEndRequest; } public void Dispose() { } private void OnBeginRequest(object sender, EventArgs e) { HttpApplication TheApplication = (HttpApplication)sender; HttpContext TheContext = TheApplication.Context; string TheRequestUrl = TheContext.Request.Url.ToString(); if (TheRequestUrl.Contains("/images/") == true && ImageCacheManager.TheImageResponseCache.ContainsKey(TheRequestUrl) == true) { byte[] cachedContent = ImageCacheManager.TheImageResponseCache[TheRequestUrl]; string contentType = DetermineContentType(TheRequestUrl); TheContext.Response.ContentType = contentType; TheContext.Response.AddHeader("Content-Length", cachedContent.Length.ToString()); TheContext.Response.BinaryWrite(cachedContent); TheContext.Response.End(); } } private string DetermineContentType(string TheRequestUrl) { string TheFileExtension = Path.GetExtension(TheRequestUrl).ToLower(); if (TheFileExtension == ".jpg" || TheFileExtension == ".jpeg") { return "image/jpeg"; } if (TheFileExtension == ".png") { return "image/png"; } if (TheFileExtension == ".svg") { return "image/jpeg"; } if (TheFileExtension == ".webp") { return "image/webp"; } return "application/octet-stream"; } private void OnEndRequest(object sender, EventArgs e) { HttpApplication TheApplication = (HttpApplication)sender; HttpContext TheContext = TheApplication.Context; string TheRequestUrl = TheContext.Request.Url.ToString(); if (TheRequestUrl.Contains("/images/") == true) { using (MemoryStream capturedStream = new MemoryStream()) { Stream originalResponseStream = TheContext.Response.Filter; TheContext.Response.Filter = capturedStream; TheContext.Response.Flush(); TheContext.Response.Filter = originalResponseStream; if (ImageCacheManager.TheImageResponseCache.ContainsKey(TheRequestUrl) == false) { ImageCacheManager.TheImageResponseCache[TheRequestUrl] = capturedStream.ToArray(); } } } } }
问题原因分析
1. 响应流捕获逻辑错误
OnEndRequest事件触发时,响应内容已经被写入到原始输出流(如IIS的输出通道),此时替换Response.Filter并调用Flush()无法将已输出的内容回读到capturedStream中,导致缓存的是空或不完整的二进制数据,这是图片损坏的核心原因。
正确的姿势是在BeginRequest阶段就替换Filter,用自定义Stream包装原始流,在写入响应的同时捕获数据。
2. SVG的MIME类型错误
DetermineContentType方法中,SVG被错误设置为image/jpeg,正确的MIME类型应为image/svg+xml。错误的Content-Type会导致浏览器无法正确解析SVG图片。
3. 非线程安全的缓存容器
Dictionary<string, byte[]>不是线程安全的,当多个并发请求同时读写缓存时,可能导致数据损坏或抛出异常,应改用ConcurrentDictionary<string, byte[]>(同时建议缓存响应头信息,比如创建一个包含二进制数据和响应头的类)。
4. 忽略响应编码处理
如果原始响应启用了内容编码(如gzip、deflate),直接缓存二进制数据后,返回时未设置对应的Content-Encoding头,浏览器无法解压数据,会导致图片显示异常。需要同时缓存响应头信息,重放时一并输出。
修复示例
自定义捕获流
public class CapturingStream : Stream { private readonly Stream _originalStream; private readonly MemoryStream _capturedStream; public CapturingStream(Stream originalStream) { _originalStream = originalStream; _capturedStream = new MemoryStream(); } public byte[] CapturedData => _capturedStream.ToArray(); // 实现Stream的抽象方法,将写入的数据同时转发到原始流和捕获流 public override void Write(byte[] buffer, int offset, int count) { _originalStream.Write(buffer, offset, count); _capturedStream.Write(buffer, offset, count); } // 其他Stream方法(如Read、Seek等)直接委托给原始流 public override bool CanRead => _originalStream.CanRead; public override bool CanSeek => _originalStream.CanSeek; public override bool CanWrite => _originalStream.CanWrite; public override long Length => _originalStream.Length; public override long Position { get => _originalStream.Position; set => _originalStream.Position = value; } public override void Flush() => _originalStream.Flush(); public override int Read(byte[] buffer, int offset, int count) => _originalStream.Read(buffer, offset, count); public override long Seek(long offset, SeekOrigin origin) => _originalStream.Seek(offset, origin); public override void SetLength(long value) => _originalStream.SetLength(value); }
修改后的模块核心逻辑
public class ModuleImageCache : IHttpModule { // 改用线程安全的容器,同时缓存响应头和数据 private static readonly ConcurrentDictionary<string, (byte[] Data, string ContentType, string ContentEncoding)> _cache = new(); public void Init(HttpApplication context) { context.BeginRequest += OnBeginRequest; context.PreSendRequestContent += OnPreSendRequestContent; } private void OnBeginRequest(object sender, EventArgs e) { var app = (HttpApplication)sender; var context = app.Context; var url = context.Request.Url.ToString(); if (url.Contains("/images/") && _cache.TryGetValue(url, out var cacheItem)) { // 重放缓存的响应 context.Response.ContentType = cacheItem.ContentType; if (!string.IsNullOrEmpty(cacheItem.ContentEncoding)) { context.Response.AddHeader("Content-Encoding", cacheItem.ContentEncoding); } context.Response.AddHeader("Content-Length", cacheItem.Data.Length.ToString()); context.Response.BinaryWrite(cacheItem.Data); context.Response.End(); return; } // 未命中缓存,替换Filter准备捕获响应 if (url.Contains("/images/")) { context.Response.Filter = new CapturingStream(context.Response.Filter); } } private void OnPreSendRequestContent(object sender, EventArgs e) { var app = (HttpApplication)sender; var context = app.Context; var url = context.Request.Url.ToString(); if (url.Contains("/images/") && context.Response.Filter is CapturingStream capturingStream) { // 捕获完整响应后存入缓存 var contentType = context.Response.ContentType; var contentEncoding = context.Response.Headers["Content-Encoding"]; _cache.TryAdd(url, (capturingStream.CapturedData, contentType, contentEncoding)); } } // 修正MIME类型的DetermineContentType方法 private string DetermineContentType(string url) { var ext = Path.GetExtension(url).ToLower(); return ext switch { ".jpg" or ".jpeg" => "image/jpeg", ".png" => "image/png", ".svg" => "image/svg+xml", ".webp" => "image/webp", _ => "application/octet-stream" }; } public void Dispose() { } }
内容的提问来源于stack exchange,提问作者frenchie
相关产品推荐
相关产品推荐

