Изменить ответ промежуточного программного обеспечения

Мое требование: написать промежуточное программное обеспечение, которое отфильтровывает все "плохие слова" из ответа, полученного от другого последующего промежуточного программного обеспечения (например, Mvc).

Проблема: потоковая передача ответа. Поэтому, когда мы вернемся к нашему FilterBadWordsMiddleware из последующего промежуточного программного обеспечения, которое уже записало ответ, мы опоздали на вечеринку... потому что ответ уже начался, что приводит к известной ошибке response has already started...

Так как это требование во многих различных ситуациях - как с этим бороться?

5 ответов

Решение

Заменить поток ответа на MemoryStream предотвратить его отправку. Вернуть исходный поток после изменения ответа:

public class EditResponseMiddleware
{
    private readonly RequestDelegate _next;

    public EditResponseMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext context)
    {
        var originBody = context.Response.Body;

        var newBody = new MemoryStream();

        context.Response.Body = newBody;

        await _next(context);

        newBody.Seek(0, SeekOrigin.Begin);

        string json = new StreamReader(newBody).ReadToEnd();

        context.Response.Body = originBody;

        await context.Response.WriteAsync(modifiedJson);
    }
}

Это обходной путь, и он может вызвать проблемы с производительностью. Я надеюсь увидеть лучшее решение здесь.

Более простая версия, основанная на коде, который я использовал:

/// <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;
    }
}

К сожалению, мне не разрешено комментировать, так как моя оценка слишком низкая. Так что просто хотел опубликовать свое расширение отличного топового решения и модификацию для.NET Core 3.0+

Прежде всего

context.Request.EnableRewind();

был изменен на

context.Request.EnableBuffering();

в.net Core 3.0+

А вот как я читаю / записываю содержимое тела:

Сначала фильтр, поэтому мы просто изменяем интересующие нас типы контента.

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
    ReturnBody(context.Response, originBody);
}

"Настоящий" производственный сценарий можно найти здесь: промежуточное ПО tethys logging

Если вы следуете логике, представленной в ссылке, не забудьте добавитьhttpContext.Request.EnableRewind() предварительный вызов _next(httpContext) (метод расширения Microsoft.AspNetCore.Http.Internal пространство имен).

Если вы используете MVC, вы можете попробовать фильтры . Кажется, они позволяют изменить ответ.

Другие вопросы по тегам