File: ResponseBufferingStream.cs
Web Access
Project: src\src\Middleware\HttpLogging\src\Microsoft.AspNetCore.HttpLogging.csproj (Microsoft.AspNetCore.HttpLogging)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Buffers;
using System.IO.Pipelines;
using System.Text;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.Logging;
 
namespace Microsoft.AspNetCore.HttpLogging;
 
internal sealed class ResponseBufferingStream : BufferingStream, IHttpResponseBodyFeature
{
    private readonly IHttpResponseBodyFeature _innerBodyFeature;
    private int _limit;
    private PipeWriter? _pipeAdapter;
 
    private readonly HttpLoggingInterceptorContext _logContext;
    private readonly HttpLoggingOptions _options;
    private readonly IHttpLoggingInterceptor[] _interceptors;
    private bool _logBody;
    private Encoding? _encoding;
 
    private static readonly StreamPipeWriterOptions _pipeWriterOptions = new StreamPipeWriterOptions(leaveOpen: true);
 
    internal ResponseBufferingStream(IHttpResponseBodyFeature innerBodyFeature,
        ILogger logger,
        HttpLoggingInterceptorContext logContext,
        HttpLoggingOptions options,
        IHttpLoggingInterceptor[] interceptors)
        : base(innerBodyFeature.Stream, logger)
    {
        _innerBodyFeature = innerBodyFeature;
        _innerStream = innerBodyFeature.Stream;
        _logContext = logContext;
        _options = options;
        _interceptors = interceptors;
    }
 
    public bool HeadersWritten { get; private set; }
 
    public Stream Stream => this;
 
    public PipeWriter Writer => _pipeAdapter ??= PipeWriter.Create(Stream, _pipeWriterOptions);
 
    public override void Write(byte[] buffer, int offset, int count)
    {
        Write(buffer.AsSpan(offset, count));
    }
 
    public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state)
    {
        return TaskToApm.Begin(WriteAsync(buffer, offset, count), callback, state);
    }
 
    public override void EndWrite(IAsyncResult asyncResult)
    {
        TaskToApm.End(asyncResult);
    }
 
    public override void Write(ReadOnlySpan<byte> span)
    {
        OnFirstWriteSync();
        CommonWrite(span);
 
        _innerStream.Write(span);
    }
 
    public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
    {
        await WriteAsync(new Memory<byte>(buffer, offset, count), cancellationToken);
    }
 
    public override async ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default)
    {
        await OnFirstWriteAsync();
        CommonWrite(buffer.Span);
 
        await _innerStream.WriteAsync(buffer, cancellationToken);
    }
 
    private void CommonWrite(ReadOnlySpan<byte> span)
    {
        var remaining = _limit - _bytesBuffered;
        var innerCount = Math.Min(remaining, span.Length);
 
        if (_logBody && innerCount > 0)
        {
            var slice = span.Slice(0, innerCount);
            if (slice.TryCopyTo(_tailMemory.Span))
            {
                _tailBytesBuffered += innerCount;
                _bytesBuffered += innerCount;
                _tailMemory = _tailMemory.Slice(innerCount);
            }
            else
            {
                BuffersExtensions.Write(this, slice);
            }
        }
    }
 
    private void OnFirstWriteSync()
    {
        if (!HeadersWritten)
        {
            // Log headers as first write occurs (headers locked now)
            HttpLoggingMiddleware.LogResponseHeadersSync(_logContext, _options, _interceptors, _logger);
            OnFirstWriteCore();
        }
    }
 
    private async ValueTask OnFirstWriteAsync()
    {
        if (!HeadersWritten)
        {
            // Log headers as first write occurs (headers locked now)
            await HttpLoggingMiddleware.LogResponseHeadersAsync(_logContext, _options, _interceptors, _logger);
            OnFirstWriteCore();
        }
    }
 
    private void OnFirstWriteCore()
    {
        // The callback in LogResponseHeaders could disable body logging or adjust limits.
        if (_logContext.LoggingFields.HasFlag(HttpLoggingFields.ResponseBody) && _logContext.ResponseBodyLogLimit > 0)
        {
            if (MediaTypeHelpers.TryGetEncodingForMediaType(_logContext.HttpContext.Response.ContentType,
                _options.MediaTypeOptions.MediaTypeStates, out _encoding))
            {
                _logBody = true;
                _limit = _logContext.ResponseBodyLogLimit;
            }
            else
            {
                _logger.UnrecognizedMediaType("response");
            }
        }
 
        HeadersWritten = true;
    }
 
    public void DisableBuffering()
    {
        _innerBodyFeature.DisableBuffering();
    }
 
    public async Task SendFileAsync(string path, long offset, long? count, CancellationToken cancellation)
    {
        await OnFirstWriteAsync();
        await _innerBodyFeature.SendFileAsync(path, offset, count, cancellation);
    }
 
    public async Task StartAsync(CancellationToken token = default)
    {
        await OnFirstWriteAsync();
        await _innerBodyFeature.StartAsync(token);
    }
 
    public async Task CompleteAsync()
    {
        await OnFirstWriteAsync();
        await _innerBodyFeature.CompleteAsync();
    }
 
    public override void Flush()
    {
        OnFirstWriteSync();
        base.Flush();
    }
 
    public override async Task FlushAsync(CancellationToken cancellationToken)
    {
        await OnFirstWriteAsync();
        await base.FlushAsync(cancellationToken);
    }
 
    public void LogResponseBody()
    {
        if (_logBody)
        {
            var responseBody = GetString(_encoding!);
            _logger.ResponseBody(responseBody);
        }
    }
 
    public void LogResponseBody(HttpLoggingInterceptorContext logContext)
    {
        if (_logBody)
        {
            logContext.AddParameter("ResponseBody", GetString(_encoding!));
        }
    }
}