Response body for request/response Logging - c#

I'm trying to write a Owin midleware component that would LOG every incoming request and response to the database.
Here's how far I managed to get.
I got stuck on reading the response.body. Says:
Stream does not support reading.
How can I read the Response.Body ?
public class LoggingMiddleware : OwinMiddleware
{
private static Logger log = LogManager.GetLogger("WebApi");
public LoggingMiddleware(OwinMiddleware next, IAppBuilder app)
: base(next)
{
}
public override async Task Invoke(IOwinContext context)
{
using (var db = new HermesEntities())
{
var sw = new Stopwatch();
sw.Start();
var logRequest = new log_Request
{
Body = new StreamReader(context.Request.Body).ReadToEndAsync().Result,
Headers = Json.Encode(context.Request.Headers),
IPTo = context.Request.LocalIpAddress,
IpFrom = context.Request.RemoteIpAddress,
Method = context.Request.Method,
Service = "Api",
Uri = context.Request.Uri.ToString(),
UserName = context.Request.User.Identity.Name
};
db.log_Request.Add(logRequest);
context.Request.Body.Position = 0;
await Next.Invoke(context);
var mem2 = new MemoryStream();
await context.Response.Body.CopyToAsync(mem2);
var logResponse = new log_Response
{
Headers = Json.Encode(context.Response.Headers),
Body = new StreamReader(mem2).ReadToEndAsync().Result,
ProcessingTime = sw.Elapsed,
ResultCode = context.Response.StatusCode,
log_Request = logRequest
};
db.log_Response.Add(logResponse);
await db.SaveChangesAsync();
}
}
}

Response Body can be logged in this manner:
public class LoggingMiddleware : OwinMiddleware
{
private static Logger log = LogManager.GetLogger("WebApi");
public LoggingMiddleware(OwinMiddleware next, IAppBuilder app)
: base(next)
{
}
public override async Task Invoke(IOwinContext context)
{
using (var db = new HermesEntities())
{
var sw = new Stopwatch();
sw.Start();
var logRequest = new log_Request
{
Body = new StreamReader(context.Request.Body).ReadToEndAsync().Result,
Headers = Json.Encode(context.Request.Headers),
IPTo = context.Request.LocalIpAddress,
IpFrom = context.Request.RemoteIpAddress,
Method = context.Request.Method,
Service = "Api",
Uri = context.Request.Uri.ToString(),
UserName = context.Request.User.Identity.Name
};
db.log_Request.Add(logRequest);
context.Request.Body.Position = 0;
Stream stream = context.Response.Body;
MemoryStream responseBuffer = new MemoryStream();
context.Response.Body = responseBuffer;
await Next.Invoke(context);
responseBuffer.Seek(0, SeekOrigin.Begin);
var responseBody = new StreamReader(responseBuffer).ReadToEnd();
//do logging
var logResponse = new log_Response
{
Headers = Json.Encode(context.Response.Headers),
Body = responseBody,
ProcessingTime = sw.Elapsed,
ResultCode = context.Response.StatusCode,
log_Request = logRequest
};
db.log_Response.Add(logResponse);
responseBuffer.Seek(0, SeekOrigin.Begin);
await responseBuffer.CopyToAsync(stream);
await db.SaveChangesAsync();
}
}
}

Response body is a write-only network stream by default for Katana hosts. You will need to replace it with a MemoryStream, read the stream, log the content and then copy the memory stream content back into the original network stream. BTW, if your middleware reads the request body, downstream components cannot, unless the request body is buffered. So, you might need to consider buffering the request body as well. If you want to look at some code, http://lbadri.wordpress.com/2013/08/03/owin-authentication-middleware-for-hawk-in-thinktecture-identitymodel-45/ could be a starting point. Look at the class HawkAuthenticationHandler.

I've solved the problem by applying an action attribute writing the request body to OWIN environment dictionary. After that, the logging middleware can access it by a key.
public class LogResponseBodyInterceptorAttribute : ActionFilterAttribute
{
public override async Task OnActionExecutedAsync(HttpActionExecutedContext actionExecutedContext, CancellationToken cancellationToken)
{
if (actionExecutedContext?.Response?.Content is ObjectContent)
{
actionExecutedContext.Request.GetOwinContext().Environment["log-responseBody"] =
await actionExecutedContext.Response.Content.ReadAsStringAsync();
}
}
}
And then in the middleware:
public class RequestLoggingMiddleware
{
...
private void LogResponse(IOwinContext owinContext)
{
var message = new StringBuilder()
.AppendLine($"{owinContext.Response.StatusCode}")
.AppendLine(string.Join(Environment.NewLine, owinContext.Response.Headers.Select(x => $"{x.Key}: {string.Join("; ", x.Value)}")));
if (owinContext.Environment.ContainsKey("log-responseBody"))
{
var responseBody = (string)owinContext.Environment["log-responseBody"];
message.AppendLine()
.AppendLine(responseBody);
}
var logEvent = new LogEventInfo
{
Level = LogLevel.Trace,
Properties =
{
{"correlationId", owinContext.Environment["correlation-id"]},
{"entryType", "Response"}
},
Message = message.ToString()
};
_logger.Log(logEvent);
}
}

If you're facing the issue where the Stream does not support reading error occurs when trying to read the request body more than once, you can try the following workaround.
In your Startup.cs file, add the following middleware to enable buffering of the request body, which allows you to re-read the request body for logging purposes:
app.Use(async (context, next) =>
{
using (var streamCopy = new MemoryStream())
{
await context.Request.Body.CopyToAsync(streamCopy);
streamCopy.Position = 0;
string body = new StreamReader(streamCopy).ReadToEnd();
streamCopy.Position = 0;
context.Request.Body = streamCopy;
await next();
}
});
This middleware creates a copy of the request body stream, reads the entire stream into a string, sets the stream position back to the beginning, sets the request body to the copied stream, and then calls the next middleware.
After this middleware, you can now use context.Request.Body.Position = 0; to set the position of the request body stream back to the beginning so you can re-read the request body.
I hope this helps!

Related

Replacing Request/Response Body in asp.net core middleware

I want to replace the request/response body in my middleware. Suppose if client sends Hello, I want to change it to Hola and similarly with response. I found the code that works but not to my requirement. My question is why this works and the other do not. It is actually the same code.
Working Code
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
var request = context.Request;
var stream = request.Body;// currently holds the original stream
var originalReader = new StreamReader(stream);
var originalContent = await originalReader.ReadToEndAsync();
var notModified = true;
try
{
if (originalContent != null)
{
//Master is some model name
var modifiedData = new Master() { Id = "Changed in Middleware", Email = "Changed" };
var json = JsonConvert.SerializeObject(modifiedData);
//json variable is just a string here
//modified stream
var requestData = Encoding.UTF8.GetBytes(json);
stream = new MemoryStream(requestData);
notModified = false;
}
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
if (notModified)
{
//putting original data
var requestData = Encoding.UTF8.GetBytes(originalContent);
stream = new MemoryStream(requestData);
}
request.Body = stream;
await next(context);
}
Not working Code But My Requirement
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
var request = context.Request;
var stream = request.Body;// currently holds the original stream
var originalReader = new StreamReader(stream);
var originalContent = await originalReader.ReadToEndAsync();
var notModified = true;
try
{
if (originalContent != null)
{
var json = "This is just a string as deserializing returns string";
//modified stream
var requestData = Encoding.UTF8.GetBytes(json);
stream = new MemoryStream(requestData);
notModified = false;
}
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
if (notModified)
{
//putting original data
var requestData = Encoding.UTF8.GetBytes(originalContent);
stream = new MemoryStream(requestData);
}
request.Body = stream;
await next(context);
}
So the working code pass on the changes to the controller, and has value Id ="Changed in middlware" and Email = "changed" but the not working code doesnt pass anything ... in the controller, argument has null and not the "This is just a string as deserializing returns string" value. I know this is not some magic happening. Am i missing something?
You need to convert string to jsontype.Try to change your code like this:
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
var request = context.Request;
var stream = request.Body;// currently holds the original stream
var originalReader = new StreamReader(stream);
var originalContent = await originalReader.ReadToEndAsync();
var notModified = true;
try
{
if (originalContent != null)
{
var str = "This is just a string as deserializing returns string";
//convert string to jsontype
var json = JsonConvert.SerializeObject(str);
//modified stream
var requestData = Encoding.UTF8.GetBytes(json);
stream = new MemoryStream(requestData);
notModified = false;
}
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
if (notModified)
{
//putting original data
var requestData = Encoding.UTF8.GetBytes(originalContent);
stream = new MemoryStream(requestData);
}
request.Body = stream;
await next(context);
}
TestRequestBody Action:
public IActionResult TestRequestBody([FromBody] string s)
{
return Ok();
}
result:

How to read the HttpResponse Body?

I have a .NET Core Web API project with a request and response logging middleware. I registered both middleware files in the Configure method in the Startup file
app.UseMiddleware<RequestLoggingMiddleware>();
app.UseMiddleware<ResponseLoggingMiddleware>();
For now I'm just trying to log the body, the request logging seems to work fine
public class RequestLoggingMiddleware
{
private readonly RequestDelegate requestDelegate;
public RequestLoggingMiddleware(RequestDelegate requestDelegate)
{
this.requestDelegate = requestDelegate;
}
public async Task InvokeAsync(HttpContext httpContext)
{
HttpRequest httpRequest = httpContext.Request;
httpRequest.EnableBuffering();
ReadResult bodyReadResult = await httpRequest.BodyReader.ReadAsync();
ReadOnlySequence<byte> bodyBuffer = bodyReadResult.Buffer;
if (bodyBuffer.Length > 0)
{
byte[] bodyBytes = bodyBuffer.ToArray();
string bodyText = Encoding.UTF8.GetString(bodyBytes);
Console.WriteLine(bodyText);
}
// Reset
httpRequest.Body.Seek(0, SeekOrigin.Begin);
await requestDelegate(httpContext);
}
}
My response logging middleware does not have access to a BodyReader. I tried to go with this code
public class ResponseLoggingMiddleware
{
private readonly RequestDelegate requestDelegate;
public ResponseLoggingMiddleware(RequestDelegate requestDelegate)
{
this.requestDelegate = requestDelegate;
}
public async Task InvokeAsync(HttpContext httpContext)
{
await requestDelegate(httpContext);
Stream responseBody = httpContext.Response.Body;
using (StreamReader reader = new StreamReader(responseBody))
{
string bodyText = await reader.ReadToEndAsync();
// Reset
responseBody.Seek(0, SeekOrigin.Begin);
Console.WriteLine(bodyText);
}
}
}
but unfortunately I get this exception
System.ArgumentException: Stream was not readable.
Does someone know how to fix it?
You may use StreamReader to read the request body. Below code, you may follow.
string body = string.Empty;
Request.EnableRewind();
using (var reader = new StreamReader(Request.Body))
{
Request.Body.Seek(0, SeekOrigin.Begin);
body = reader.ReadToEnd();
}
In the same way, you can get a response body.
using (var reader = new StreamReader(Response.Body))
{
Response.Body.Seek(0, SeekOrigin.Begin);
body = reader.ReadToEnd();
}
Notes: Above code is based on .net core 2.2
Below is the code supported by .net core 5
string body = string.Empty;
using (var reader = new StreamReader(Request.Body))
{
//Request.Body.Seek(0, SeekOrigin.Begin);
//body = reader.ReadToEnd();
body = await reader.ReadToEndAsync();
}
Now, you have the response in the body property, do your kinds of stuff (JSON Deserilize).
Stream is an Abstract class, you must tu use a MemoryStream, check this:
using (var ms = new MemoryStream())
{
var cuerpoOriginalRespuesta = contexto.Response.Body;
contexto.Response.Body = ms;
await siguiente(contexto);
ms.Seek(0, SeekOrigin.Begin);
string respuesta = new StreamReader(ms).ReadToEnd();
ms.Seek(0, SeekOrigin.Begin);
await ms.CopyToAsync(cuerpoOriginalRespuesta);
contexto.Response.Body = cuerpoOriginalRespuesta;
}
Anyone who is looking for the solution of .net standard can use the following snippet
For ApiController
string requestBody = string.Empty;
using (var reader = new StreamReader(HttpContext.Current.Request.InputStream))
{
reader.BaseStream.Seek(0, SeekOrigin.Begin);
requestBody = reader.ReadToEnd();
}
Console.WriteLine(requestBody);
For regular controller
string requestBody = string.Empty;
using (var reader = new StreamReader(HttpContext.Request.InputStream))
{
reader.BaseStream.Seek(0, SeekOrigin.Begin);
requestBody = reader.ReadToEnd();
}
Console.WriteLine(requestBody);

Capture all requests to Web Api 2.0, regardless that are mapped or not

I have a Web API 2.0 running at localhost:4512 and I want to intercept all requests made to domain localhost:4512 regardless if they are handled by a specific route or not.
For instance I want to capture requests made to localhost:4512/abc.dfsada or localhost:4512/meh/abc.js
I have tried this with a DelegatingHandler but unfortunately this only intercepts requests made to handled routes:
public class ProxyHandler : DelegatingHandler
{
private async Task<HttpResponseMessage> RedirectRequest(HttpRequestMessage request, CancellationToken cancellationToken)
{
var redirectLocation = "http://localhost:54957/";
var localPath = request.RequestUri.LocalPath;
var client = new HttpClient();
var clonedRequest = await request.Clone();
clonedRequest.RequestUri = new Uri(redirectLocation + localPath);
return await client.SendAsync(clonedRequest, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
}
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
return RedirectRequest(request, cancellationToken);
}
}
and in WebConfig.cs:
config.MessageHandlers.Add(new ProxyHandler());
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new {id = RouteParameter.Optional});
You can use the Application_BeginRequest method in the Global.asax class. It will be invoked first when the application recieves a request
Here's an example:
protected void Application_BeginRequest(object sender, EventArgs e)
{
var request = ((System.Web.HttpApplication) sender).Request;
}
I found that the best way to do what you want to do is by using the middleware to intercept all the request and responses.
public class RequestResponseLoggingMiddleware
{
private readonly RequestDelegate _next;
public RequestResponseLoggingMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
//First, get the incoming request
var request = await FormatRequest(context.Request);
//Copy a pointer to the original response body stream
var originalBodyStream = context.Response.Body;
//Create a new memory stream...
using (var responseBody = new MemoryStream())
{
//...and use that for the temporary response body
context.Response.Body = responseBody;
//Continue down the Middleware pipeline, eventually returning to this class
await _next(context);
//Format the response from the server
var response = await FormatResponse(context.Response);
//TODO: Save log to chosen datastore
//Copy the contents of the new memory stream (which contains the response) to the original stream, which is then returned to the client.
await responseBody.CopyToAsync(originalBodyStream);
}
}
private async Task<string> FormatRequest(HttpRequest request)
{
var body = request.Body;
//This line allows us to set the reader for the request back at the beginning of its stream.
request.EnableRewind();
//We now need to read the request stream. First, we create a new byte[] with the same length as the request stream...
var buffer = new byte[Convert.ToInt32(request.ContentLength)];
//...Then we copy the entire request stream into the new buffer.
await request.Body.ReadAsync(buffer, 0, buffer.Length);
//We convert the byte[] into a string using UTF8 encoding...
var bodyAsText = Encoding.UTF8.GetString(buffer);
//..and finally, assign the read body back to the request body, which is allowed because of EnableRewind()
request.Body = body;
return $"{request.Scheme} {request.Host}{request.Path} {request.QueryString} {bodyAsText}";
}
private async Task<string> FormatResponse(HttpResponse response)
{
//We need to read the response stream from the beginning...
response.Body.Seek(0, SeekOrigin.Begin);
//...and copy it into a string
string text = await new StreamReader(response.Body).ReadToEndAsync();
//We need to reset the reader for the response so that the client can read it.
response.Body.Seek(0, SeekOrigin.Begin);
//Return the string for the response, including the status code (e.g. 200, 404, 401, etc.)
return $"{response.StatusCode}: {text}";
}
Don't forget to use the middleware in the startup.cs
public void Configure(IApplicationBuilder app, IHostingEnvironment env )
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseHsts();
}
app.UseMiddleware<RequestResponseLoggingMiddleware>();
app.UseHttpsRedirection();
app.UseMvc();
}

getting the request body inside HttpContext from a Middleware in asp.net core 2.0

I am having a simple middleware which fetches the body of the request and store it in a string. It is reading fine the stream, but the issue is it wont call my controller which called just after I read the stream and throw the error
A non-empty request body is required
. Below is my code.
public async Task Invoke(HttpContext httpContext)
{
var timer = Stopwatch.StartNew();
ReadBodyFromHttpContext(httpContext);
await _next(httpContext);
timer.Stop();
}
private string ReadBodyFromHttpContext(HttpContext httpContext)
{
return await new StreamReader(httpContext.Request.Body).ReadToEndAsync();
}
You need to convert HttpContext.Request.Body from a forward only memory stream to a seekable stream, shown below.
// Enable seeking
context.Request.EnableBuffering();
// Read the stream as text
var bodyAsText = await new System.IO.StreamReader(context.Request.Body).ReadToEndAsync();
// Set the position of the stream to 0 to enable rereading
context.Request.Body.Position = 0;
when it comes to capturing the body of an HTTP request and/or response, this is no trivial effort. In ASP .NET Core, the body is a stream – once you consume it (for logging, in this case), it’s gone, rendering the rest of the pipeline useless.
Ref:http://www.palador.com/2017/05/24/logging-the-body-of-http-request-and-response-in-asp-net-core/
public async Task Invoke(HttpContext httpContext)
{
var timer = Stopwatch.StartNew();
string bodyAsText = await new StreamReader(httpContext.Request.Body).ReadToEndAsync();
var injectedRequestStream = new MemoryStream();
var bytesToWrite = Encoding.UTF8.GetBytes(bodyAsText);
injectedRequestStream.Write(bytesToWrite, 0, bytesToWrite.Length);
injectedRequestStream.Seek(0, SeekOrigin.Begin);
httpContext.Request.Body = injectedRequestStream;
await _next(httpContext);
timer.Stop();
}
Few things are crucial here:
enable buffering
last flag leaveOpen in StreamReader
reset request body stream position (SeekOrigin.Begin)
public void UseMyMiddleware(IApplicationBuilder app)
{
app.Use(async (context, next) =>
{
context.Request.EnableBuffering();
using (var reader = new StreamReader(context.Request.Body, Encoding.UTF8, false, 1024, true))
{
var body = await reader.ReadToEndAsync();
context.Request.Body.Seek(0, SeekOrigin.Begin);
}
await next.Invoke();
});
}
using (var mem = new MemoryStream())
using (var reader = new StreamReader(mem))
{
Request.Body.CopyTo(mem);
var body = reader.ReadToEnd();
//and this you can reset the position of the stream.
mem.Seek(0, SeekOrigin.Begin);
body = reader.ReadToEnd();
}
Here you are can read how it works. https://gunnarpeipman.com/aspnet-core-request-body/
You can try this
public async Task Invoke(HttpContext context)
{
var request = context.Request;
request.EnableBuffering();
var buffer = new byte[Convert.ToInt32(request.ContentLength)];
await request.Body.ReadAsync(buffer, 0, buffer.Length);
var requestContent = Encoding.UTF8.GetString(buffer);
request.Body.Position = 0; //rewinding the stream to 0
}

Modify OWIN/Katana PhysicalFileSystem page on request

I have a self-hosted app that's using OWIN to provide a basic web server. The key part of the configuration is the following line:
appBuilder.UseFileServer(new FileServerOptions {
FileSystem = new PhysicalFileSystem(filePath)
});
This provides the static files listed in the filePath for browsing, and this much is working as expected.
However I've run into a case where I want to slightly modify one of the files on a request-by-request basis. In particular, I want to load the "normal" version of the file from the filesystem, alter it slightly based on the incoming web request's headers, and then return the altered version to the client instead of the original. All other files should remain unmodified.
How do I go about doing this?
Well, I don't know if this is a good way to do this, but it seems to work:
internal class FileReplacementMiddleware : OwinMiddleware
{
public FileReplacementMiddleware(OwinMiddleware next) : base(next) {}
public override async Task Invoke(IOwinContext context)
{
MemoryStream memStream = null;
Stream httpStream = null;
if (ShouldAmendResponse(context))
{
memStream = new MemoryStream();
httpStream = context.Response.Body;
context.Response.Body = memStream;
}
await Next.Invoke(context);
if (memStream != null)
{
var content = await ReadStreamAsync(memStream);
if (context.Response.StatusCode == 200)
{
content = AmendContent(context, content);
}
var contentBytes = Encoding.UTF8.GetBytes(content);
context.Response.Body = httpStream;
context.Response.ETag = null;
context.Response.ContentLength = contentBytes.Length;
await context.Response.WriteAsync(contentBytes, context.Request.CallCancelled);
}
}
private static async Task<string> ReadStreamAsync(MemoryStream stream)
{
stream.Seek(0, SeekOrigin.Begin);
using (var reader = new StreamReader(stream, Encoding.UTF8))
{
return await reader.ReadToEndAsync();
}
}
private bool ShouldAmendResponse(IOwinContext context)
{
// logic
}
private string AmendContent(IOwinContext context, string content)
{
// logic
}
}
Add this to the pipeline somewhere before the static files middleware.

Categories