I've been struggling to get the Response.Body
property from an ASP.NET Core action and the only solution I've been able to identify seems sub-optimal. The solution requires swapping out Response.Body
with a MemoryStream
while reading the stream into a string variable, then swapping it back before sending to the client. In the examples below, I'm trying to get the Response.Body
value in a custom middleware class. Response.Body
is a set only property in ASP.NET Core for some reason? Am I just missing something here, or is this an oversight/bug/design issue? Is there a better way to read Response.Body
?
Current (sub-optimal) solution:
public class MyMiddleWare { private readonly RequestDelegate _next; public MyMiddleWare(RequestDelegate next) { _next = next; } public async Task Invoke(HttpContext context) { using (var swapStream = new MemoryStream()) { var originalResponseBody = context.Response.Body; context.Response.Body = swapStream; await _next(context); swapStream.Seek(0, SeekOrigin.Begin); string responseBody = new StreamReader(swapStream).ReadToEnd(); swapStream.Seek(0, SeekOrigin.Begin); await swapStream.CopyToAsync(originalResponseBody); context.Response.Body = originalResponseBody; } } }
Attempted solution using EnableRewind(): This only works for Request.Body
, not Response.Body
. This results in reading an empty string from Response.Body
rather than the actual response body contents.
Startup.cs
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, IApplicationLifetime appLifeTime) { loggerFactory.AddConsole(Configuration.GetSection("Logging")); loggerFactory.AddDebug(); app.Use(async (context, next) => { context.Request.EnableRewind(); await next(); }); app.UseMyMiddleWare(); app.UseMvc(); // Dispose of Autofac container on application stop appLifeTime.ApplicationStopped.Register(() => this.ApplicationContainer.Dispose()); }
MyMiddleWare.cs
public class MyMiddleWare { private readonly RequestDelegate _next; public MyMiddleWare(RequestDelegate next) { _next = next; } public async Task Invoke(HttpContext context) { await _next(context); string responseBody = new StreamReader(context.Request.Body).ReadToEnd(); //responseBody is "" context.Request.Body.Position = 0; } }
When the server receives an HTTP request, it processes that request and responds to the client. The response tells the client if the request was successful or not. ASP.NET Core is a web application framework that runs on the server.
To get the response body as a string we can use the EntityUtils. toString() method. This method read the content of an HttpEntity object content and return it as a string. The content will be converted using the character set from the entity object.
The ASP Response object is used to send output to the user from the server.
Using [FromBody] When a parameter has [FromBody], Web API uses the Content-Type header to select a formatter. In this example, the content type is "application/json" and the request body is a raw JSON string (not a JSON object). At most one parameter is allowed to read from the message body.
In my original response I had totally misread the question and thought the poster was asking how to read the Request.Body
But he had asked how to read the Response.Body
. I'm leaving my original answer to preserve history but also updating it to show how I would answer the question once reading it correctly.
Original Answer
If you want a buffered stream that supports reading multiple times you need to set
context.Request.EnableRewind()
Ideally do this early in the middleware before anything needs to read the body.
So for example you could place the following code in the beginning of the Configure
method of the Startup.cs file:
app.Use(async (context, next) => { context.Request.EnableRewind(); await next(); });
Prior to enabling Rewind the stream associated with the Request.Body
is a forward only stream that doesn't support seeking or reading the stream a second time. This was done to make the default configuration of request handling as lightweight and performant as possible. But once you enable rewind the stream is upgrade to a stream that supports seeking and reading multiple times. You can observe this "upgrade" by setting a breakpoint just before and just after the call to EnableRewind
and observing the Request.Body
properties. So for example Request.Body.CanSeek
will change from false
to true
.
update: Starting in ASP.NET Core 2.1 Request.EnableBuffering()
is available which upgrades the Request.Body
to a FileBufferingReadStream
just like Request.EnableRewind()
and since Request.EnableBuffering()
is in a public namespace rather than an internal one it should be preferred over EnableRewind(). (Thanks to @ArjanEinbu for pointing out)
Then to read the body stream you could for example do this:
string bodyContent = new StreamReader(Request.Body).ReadToEnd();
Don't wrap the StreamReader
creation in a using statement though or it will close the underlying body stream at the conclusion of the using block and code later in the request lifecycle wont be able to read the body.
Also just to be safe, it might be a good idea to follow the above line of code that reads the body content with this line of code to reset the body's stream position back to 0.
request.Body.Position = 0;
That way any code later in the request lifecycle will find the request.Body in a state just like it hasn't been read yet.
Updated Answer
Sorry I originally misread your question. The concept of upgrading the associated stream to be a buffered stream still applies. However you do have to do it manually, I'm unaware of any built in .Net Core functionality that lets you read the response stream once written in the way that EnableRewind()
lets a developer reread the request stream after it's been read.
Your "hacky" approach is likely totally appropriate. You are basically converting a stream that can't seek to one that can. At the end of the day the Response.Body
stream has to get swapped out with a stream that is buffered and supports seeking. Here is another take on middleware to do that but you will notice it's quite similar to your approach. I did however choose to use a finally block as added protection for putting the original stream back on the Response.Body
and I used the Position
property of the stream rather than the Seek
method since the syntax is a bit simpler but the effect is no different than your approach.
public class ResponseRewindMiddleware { private readonly RequestDelegate next; public ResponseRewindMiddleware(RequestDelegate next) { this.next = next; } public async Task Invoke(HttpContext context) { Stream originalBody = context.Response.Body; try { using (var memStream = new MemoryStream()) { context.Response.Body = memStream; await next(context); memStream.Position = 0; string responseBody = new StreamReader(memStream).ReadToEnd(); memStream.Position = 0; await memStream.CopyToAsync(originalBody); } } finally { context.Response.Body = originalBody; } } }
You can use a middleware in the request pipeline, in order to log request and responses.
However is increased the hazard of memory leak
, due to the facth that: 1. Streams, 2. Setting Byte Buffers and 3. String conversions
can end up to Large Object Heap (in case the body of request or response is larger than 85,000 bytes). This increases the hazard of memory leak in your application. In order to avoid LOH, memory streams can be replaced by Recyclable Memory stream using the relevant library.
An implementation that uses Recyclable memory streams:
public class RequestResponseLoggingMiddleware { private readonly RequestDelegate _next; private readonly ILogger _logger; private readonly RecyclableMemoryStreamManager _recyclableMemoryStreamManager; private const int ReadChunkBufferLength = 4096; public RequestResponseLoggingMiddleware(RequestDelegate next, ILoggerFactory loggerFactory) { _next = next; _logger = loggerFactory .CreateLogger<RequestResponseLoggingMiddleware>(); _recyclableMemoryStreamManager = new RecyclableMemoryStreamManager(); } public async Task Invoke(HttpContext context) { LogRequest(context.Request); await LogResponseAsync(context); } private void LogRequest(HttpRequest request) { request.EnableRewind(); using (var requestStream = _recyclableMemoryStreamManager.GetStream()) { request.Body.CopyTo(requestStream); _logger.LogInformation($"Http Request Information:{Environment.NewLine}" + $"Schema:{request.Scheme} " + $"Host: {request.Host} " + $"Path: {request.Path} " + $"QueryString: {request.QueryString} " + $"Request Body: {ReadStreamInChunks(requestStream)}"); } } private async Task LogResponseAsync(HttpContext context) { var originalBody = context.Response.Body; using (var responseStream = _recyclableMemoryStreamManager.GetStream()) { context.Response.Body = responseStream; await _next.Invoke(context); await responseStream.CopyToAsync(originalBody); _logger.LogInformation($"Http Response Information:{Environment.NewLine}" + $"Schema:{context.Request.Scheme} " + $"Host: {context.Request.Host} " + $"Path: {context.Request.Path} " + $"QueryString: {context.Request.QueryString} " + $"Response Body: {ReadStreamInChunks(responseStream)}"); } context.Response.Body = originalBody; } private static string ReadStreamInChunks(Stream stream) { stream.Seek(0, SeekOrigin.Begin); string result; using (var textWriter = new StringWriter()) using (var reader = new StreamReader(stream)) { var readChunk = new char[ReadChunkBufferLength]; int readChunkLength; //do while: is useful for the last iteration in case readChunkLength < chunkLength do { readChunkLength = reader.ReadBlock(readChunk, 0, ReadChunkBufferLength); textWriter.Write(readChunk, 0, readChunkLength); } while (readChunkLength > 0); result = textWriter.ToString(); } return result; } }
NB. The hazard of LOH is not fully eradicate due to textWriter.ToString()
on the other hand you can use a logging client library that supports structured logging (ie. Serilog) and inject the instance of a Recyclable Memory Stream.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With