Skip to content

Commit 69ce01a

Browse files
committed
Fix HttpLoggingMiddleware Request/Response bodies logging in case of stream being closed by a subsequent middleware (dotnet#61489)
1 parent 9878dd9 commit 69ce01a

File tree

3 files changed

+111
-4
lines changed

3 files changed

+111
-4
lines changed

src/Middleware/HttpLogging/src/RequestBufferingStream.cs

+22-2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ internal sealed class RequestBufferingStream : BufferingStream
1313
private readonly bool _logOnFinish;
1414
private readonly int _limit;
1515
private BodyStatus _status = BodyStatus.None;
16+
private string? _bodyBeforeClose;
1617

1718
public bool HasLogged { get; private set; }
1819

@@ -116,15 +117,15 @@ public void LogRequestBody()
116117
if (!HasLogged && _logOnFinish)
117118
{
118119
HasLogged = true;
119-
_logger.RequestBody(GetString(_encoding), GetStatus(showCompleted: false));
120+
_logger.RequestBody(GetStringInternal(), GetStatus(showCompleted: false));
120121
}
121122
}
122123

123124
public void LogRequestBody(HttpLoggingInterceptorContext logContext)
124125
{
125126
if (logContext.IsAnyEnabled(HttpLoggingFields.RequestBody))
126127
{
127-
logContext.AddParameter("RequestBody", GetString(_encoding));
128+
logContext.AddParameter("RequestBody", GetStringInternal());
128129
logContext.AddParameter("RequestBodyStatus", GetStatus(showCompleted: true));
129130
}
130131
}
@@ -138,6 +139,25 @@ public void LogRequestBody(HttpLoggingInterceptorContext logContext)
138139
_ => throw new NotImplementedException(_status.ToString()),
139140
};
140141

142+
private string GetStringInternal()
143+
{
144+
var result = _bodyBeforeClose ?? GetString(_encoding);
145+
// Reset the value after its consumption to preserve GetString(encoding) behavior
146+
_bodyBeforeClose = null;
147+
return result;
148+
}
149+
150+
public override void Close()
151+
{
152+
if (!HasLogged)
153+
{
154+
// Subsequent middleware can close the request stream after reading enough bytes (guided by ContentLength).
155+
// Preserving the body for the final GetStringInternal() call.
156+
_bodyBeforeClose = GetString(_encoding);
157+
}
158+
base.Close();
159+
}
160+
141161
public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state)
142162
{
143163
return TaskToApm.Begin(ReadAsync(buffer, offset, count), callback, state);

src/Middleware/HttpLogging/src/ResponseBufferingStream.cs

+23-2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ internal sealed class ResponseBufferingStream : BufferingStream, IHttpResponseBo
2020
private readonly IHttpLoggingInterceptor[] _interceptors;
2121
private bool _logBody;
2222
private Encoding? _encoding;
23+
private string? _bodyBeforeClose;
2324

2425
private static readonly StreamPipeWriterOptions _pipeWriterOptions = new StreamPipeWriterOptions(leaveOpen: true);
2526

@@ -179,7 +180,7 @@ public void LogResponseBody()
179180
{
180181
if (_logBody)
181182
{
182-
var responseBody = GetString(_encoding!);
183+
var responseBody = GetStringInternal();
183184
_logger.ResponseBody(responseBody);
184185
}
185186
}
@@ -188,7 +189,27 @@ public void LogResponseBody(HttpLoggingInterceptorContext logContext)
188189
{
189190
if (_logBody)
190191
{
191-
logContext.AddParameter("ResponseBody", GetString(_encoding!));
192+
logContext.AddParameter("ResponseBody", GetStringInternal());
192193
}
193194
}
195+
196+
private string GetStringInternal()
197+
{
198+
var result = _bodyBeforeClose ?? GetString(_encoding!);
199+
// Reset the value after its consumption to preserve GetString(encoding) behavior
200+
_bodyBeforeClose = null;
201+
return result;
202+
}
203+
204+
public override void Close()
205+
{
206+
if (_logBody)
207+
{
208+
// Subsequent middleware can close the response stream after writing its body
209+
// Preserving the body for the final GetStringInternal() call.
210+
_bodyBeforeClose = GetString(_encoding!);
211+
}
212+
213+
base.Close();
214+
}
194215
}

src/Middleware/HttpLogging/test/HttpLoggingMiddlewareTests.cs

+66
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,46 @@ public async Task RequestBodyCopyToAsyncWorks(string expected)
390390
Assert.Contains(TestSink.Writes, w => w.Message.Contains(expected));
391391
}
392392

393+
[Theory]
394+
[MemberData(nameof(BodyData))]
395+
public async Task RequestBodyWithStreamCloseWorks(string expected)
396+
{
397+
var options = CreateOptionsAccessor();
398+
options.CurrentValue.LoggingFields = HttpLoggingFields.RequestBody;
399+
400+
var middleware = CreateMiddleware(
401+
async c =>
402+
{
403+
var arr = new byte[4096];
404+
var contentLengthBytesLeft = c.Request.Body.Length;
405+
406+
// (1) The subsequent middleware reads right up to the buffer size (guided by the ContentLength header)
407+
while (contentLengthBytesLeft > 0)
408+
{
409+
var res = await c.Request.Body.ReadAsync(arr, 0, (int)Math.Min(arr.Length, contentLengthBytesLeft));
410+
contentLengthBytesLeft -= res;
411+
if (res == 0)
412+
{
413+
break;
414+
}
415+
}
416+
417+
// (2) The subsequent middleware closes the request stream after its consumption
418+
c.Request.Body.Close();
419+
},
420+
options);
421+
422+
var httpContext = new DefaultHttpContext();
423+
httpContext.Request.ContentType = "text/plain";
424+
var buffer = Encoding.UTF8.GetBytes(expected);
425+
httpContext.Request.Body = new MemoryStream(buffer);
426+
httpContext.Request.ContentLength = buffer.Length;
427+
428+
await middleware.Invoke(httpContext);
429+
430+
Assert.Contains(TestSink.Writes, w => w.Message.Contains(expected));
431+
}
432+
393433
[Fact]
394434
public async Task RequestBodyReadingLimitLongCharactersWorks()
395435
{
@@ -1155,6 +1195,32 @@ public async Task StartAsyncResponseHeadersLogged()
11551195
await middlewareTask;
11561196
}
11571197

1198+
[Theory]
1199+
[MemberData(nameof(BodyData))]
1200+
public async Task ResponseBodyWithStreamCloseWorks(string expected)
1201+
{
1202+
var options = CreateOptionsAccessor();
1203+
options.CurrentValue.LoggingFields = HttpLoggingFields.ResponseBody;
1204+
var middleware = CreateMiddleware(
1205+
async c =>
1206+
{
1207+
c.Response.ContentType = "text/plain";
1208+
1209+
// (1) The subsequent middleware writes its response
1210+
await c.Response.WriteAsync(expected);
1211+
1212+
// (2) The subsequent middleware closes the response stream after it has completed writing to it
1213+
c.Response.Body.Close();
1214+
},
1215+
options);
1216+
1217+
var httpContext = new DefaultHttpContext();
1218+
1219+
await middleware.Invoke(httpContext);
1220+
1221+
Assert.Contains(TestSink.Writes, w => w.Message.Contains(expected));
1222+
}
1223+
11581224
[Fact]
11591225
public async Task UnrecognizedMediaType()
11601226
{

0 commit comments

Comments
 (0)