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

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

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

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


person Matthias    schedule 12.06.2017    source источник


Ответы (4)


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

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

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

person Ilya Chumakov    schedule 13.06.2017
comment
Это работает, спасибо! Я обнаружил, что в некоторых случаях присоединение обратного вызова к context.Response.OnStarting() также работает, но не при изменении ответов. Также мне не нравится использовать OnStarting(), потому что он нарушает итеративный рабочий процесс промежуточного программного обеспечения. - person Matthias; 19.06.2017
comment
Для справки в будущем, прежде чем писать в Response, полезно установить свойство Content Length. - person sameerfair; 22.04.2018
comment
В Dotnet Core 3 я получаю System.InvalidOperationException: Response Content-Length mismatch, чтобы решить эту проблему. Я добавил context.Response.ContentLength = json.Length; - person Oriel Dayanim; 10.12.2019

К сожалению, мне не разрешено комментировать, так как моя оценка слишком низкая. Так что просто хотел опубликовать свое расширение отличного топового решения и модификацию для .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);
}
...

private void ReturnBody(HttpResponse response, Stream originBody)
{
    response.Body.Seek(0, SeekOrigin.Begin);
    response.Body.CopyToAsync(originBody);
    response.Body = originBody;
}
person Henric Rosvall    schedule 02.03.2020
comment
Что делает ReturnBody? - person l p; 18.07.2020
comment
Извините, в предложение решения добавлена ​​функция RetunBody - person Henric Rosvall; 12.10.2020

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

/// <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;
    }
}
person Ayushmati    schedule 04.03.2020
comment
К сожалению, я получаю System.InvalidOperationException: Response Content-Length mismatch: too few bytes written. - person Jonne Kleijer; 04.01.2021

«Настоящий» производственный сценарий можно найти здесь: промежуточное программное обеспечение для журналирования tethys

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

person Saturn Technologies    schedule 15.12.2019