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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.12 19:18:11