修改中间件响应

Modify middleware response

我的要求:编写一个中间件来过滤所有 "bad words" 来自另一个后续中间件(例如 Mvc)的响应。

问题:响应流。因此,当我们从已经写入响应的后续中间件返回我们的 FilterBadWordsMiddleware 时,我们来不及了……因为响应已经开始发送,这导致了众所周知的错误 response has already started...

既然这是在许多不同情况下的要求 -- 如何处理它?

将响应流替换为 MemoryStream 以阻止其发送。 Return响应修改后的原始流:

    public async Task Invoke(HttpContext context)
    {
        bool modifyResponse = true;
        Stream originBody = null;

        if (modifyResponse)
        {
            //uncomment this line only if you need to read context.Request.Body stream
            //context.Request.EnableRewind();

            originBody = ReplaceBody(context.Response);
        }

        await _next(context);

        if (modifyResponse)
        {
            //as we replaced the Response.Body with a MemoryStream instance before,
            //here we can read/write Response.Body
            //containing the data written by middlewares down the pipeline 

            //finally, write modified data to originBody and set it back as Response.Body value
            ReturnBody(context.Response, originBody);
        }
    }

    private Stream ReplaceBody(HttpResponse response)
    {
        var originBody = response.Body;
        response.Body = new MemoryStream();
        return originBody;
    }

    private void ReturnBody(HttpResponse response, Stream originBody)
    {
        response.Body.Seek(0, SeekOrigin.Begin);
        response.Body.CopyTo(originBody);
        response.Body = originBody;
    }

这是一种解决方法,它可能会导致性能问题。我希望在这里看到更好的解决方案。

可以在此处找到 "real" 生产方案:tethys logging middeware

如果您遵循 link 中提供的逻辑,请不要忘记在调用 _next(httpContext)Microsoft.AspNetCore.Http.Internal 命名空间的扩展方法)之前添加 httpContext.Request.EnableRewind()

很遗憾,由于我的分数太低,我无法发表评论。 所以只是想 post 我对优秀顶级解决方案的扩展,以及对 .NET Core 3.0+

的修改

首先

context.Request.EnableRewind();

已更改为

context.Request.EnableBuffering();

.NET Core 3.0+

这就是我 read/write 正文内容的方式:

首先是一个过滤器,所以我们只修改我们感兴趣的内容类型

private static readonly IEnumerable<string> validContentTypes = new HashSet<string>() { "text/html", "application/json", "application/javascript" };

这是一种将 [[[Translate me]]] 等块状文本转换为翻译的解决方案。这样我就可以标记所有需要翻译的东西,读取我们从翻译器那里得到的 po 文件,然后在输出流中进行翻译替换——不管块状文本是否在剃刀视图中,javascript 或...随便什么。 有点像 TurquoiseOwl i18n 包,但在 .NET Core 中,不幸的是,那个优秀的包不支持。

...

if (modifyResponse)
{
    //as we replaced the Response.Body with a MemoryStream instance before,
    //here we can read/write Response.Body
    //containing the data written by middlewares down the pipeline

    var contentType = context.Response.ContentType?.ToLower();
    contentType = contentType?.Split(';', StringSplitOptions.RemoveEmptyEntries).FirstOrDefault();   // Filter out text/html from "text/html; charset=utf-8"

    if (validContentTypes.Contains(contentType))
    {
        using (var streamReader = new StreamReader(context.Response.Body))
        {
            // Read the body
            context.Response.Body.Seek(0, SeekOrigin.Begin);
            var responseBody = await streamReader.ReadToEndAsync();

            // Replace [[[Bananas]]] with translated texts - or Bananas if a translation is missing
            responseBody = NuggetReplacer.ReplaceNuggets(poCatalog, responseBody);

            // Create a new stream with the modified body, and reset the content length to match the new stream
            var requestContent = new StringContent(responseBody, Encoding.UTF8, contentType);
            context.Response.Body = await requestContent.ReadAsStreamAsync();//modified stream
            context.Response.ContentLength = context.Response.Body.Length;
        }
    }

    //finally, write modified data to originBody and set it back as Response.Body value
    await ReturnBody(context.Response, originBody);
}
...

private Task ReturnBody(HttpResponse response, Stream originBody)
{
    response.Body.Seek(0, SeekOrigin.Begin);
    await response.Body.CopyToAsync(originBody);
    response.Body = originBody;
}

基于我使用的代码的更简单版本:

/// <summary>
/// The middleware Invoke method.
/// </summary>
/// <param name="httpContext">The current <see cref="HttpContext"/>.</param>
/// <returns>A Task to support async calls.</returns>
public async Task Invoke(HttpContext httpContext)
{
    var originBody = httpContext.Response.Body;
    try
    {
        var memStream = new MemoryStream();
        httpContext.Response.Body = memStream;

        await _next(httpContext).ConfigureAwait(false);

        memStream.Position = 0;
        var responseBody = new StreamReader(memStream).ReadToEnd();

        //Custom logic to modify response
        responseBody = responseBody.Replace("hello", "hi", StringComparison.InvariantCultureIgnoreCase);

        var memoryStreamModified = new MemoryStream();
        var sw = new StreamWriter(memoryStreamModified);
        sw.Write(responseBody);
        sw.Flush();
        memoryStreamModified.Position = 0;

        await memoryStreamModified.CopyToAsync(originBody).ConfigureAwait(false);
    }
    finally
    {
        httpContext.Response.Body = originBody;
    }
}