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.
Related
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);
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
}
I'm making an UWP app that uploads files to facebook, I'm using a custom HttpContent to upload the files in 4k blocks to minimize the memory usage for big files (>100mb) and to report progress.
My custom HttpContent UploadWithProgressHttpContent:
class UploadWithProgressHttpContent : HttpContent
{
private readonly IProgress<OperationProgress> _progress;
private readonly OperationProgress _data;
private readonly Stream _file;
private readonly int _bufferSize;
private readonly CancellationToken _token;
public UploadWithProgressHttpContent(
IProgress<OperationProgress> progress,
OperationProgress data,
Stream file,
int bufferSize,
CancellationToken token)
{
_progress = progress;
_data = data;
_file = file;
_bufferSize = bufferSize;
_token = token;
}
protected override Task SerializeToStreamAsync(Stream stream, TransportContext context)
{
return CopyStreamWithProgress(_file, stream, _progress, _token, _data, _bufferSize);
}
public static async Task<Stream> CopyStreamWithProgress(
Stream source,
Stream destination,
IProgress<OperationProgress> progress,
CancellationToken token,
OperationProgress progressData,
int bufferSize
)
{
int read, offset = 0;
var buffer = new byte[bufferSize];
using (source)
{
do
{
read = await source.ReadAsync(buffer, 0, bufferSize, token);
await destination.WriteAsync(buffer, 0, read, token);
offset += read;
progressData.CurrentSize = offset;
progress.Report(progressData);
} while (read != 0);
}
return destination;
}
}
What I'm experiencing (using fiddler) is that the whole file gets putted in memory before the upload starts (my progress meter reaches 100% before the upload even starts).
I did try setting the TransferEncodingChunked to true, and setting the file content length but the issue remains.
The upload source is inside a PCL (if it matters). I'm using the latest version of System.Net.Http. If need I'm using this the exact same way as it is used in the MediaFire SDK
Thanks for any help.
EDIT: Added the HttpClient usage:
public async Task<T> Upload<T>(Stream fileStream, string fileName)
{
var handler = new HttpClientHandler();
var cli = new HttpClient(handler);
foreach (var header in Headers)
{
cli.DefaultRequestHeaders.Add(header.Key, header.Value);
}
var parameters = new MultipartFormDataContent();
foreach (var parameter in Parameters)
{
parameters.Add(new StringContent(parameter.Value), parameter.Key);
}
if (fileStream != null)
{
var fileContent = new UploadWithProgressHttpContent(ProgressOperation, ProgressData, fileStream,
_chunkBufferSize, Token, fileStream.Length);
fileContent.Headers.ContentType = new MediaTypeHeaderValue(MimeTypeHelper.GetMimeType(fileName));
fileContent.Headers.ContentDisposition = new ContentDispositionHeaderValue(StreamParamName);
fileContent.Headers.ContentDisposition.FileName = fileName;
fileContent.Headers.ContentLength = fileStream.Length;
parameters.Add(fileContent, StreamParamName);
}
var req = new HttpRequestMessage(method, Path) { Content = parameters };
if (fileStream != null)
req.Headers.TransferEncodingChunked = true;
var completionOption = HttpCompletionOption.ResponseContentRead;
var resp = await cli.SendAsync(req, completionOption, Token).ConfigureAwait(false);
return await DeserializeObject<T>(resp);
}
You have the same problem as quantum mechanics - the act of observing changes the observed. Fiddler does not support request streaming - see
Fiddler makes HttpWebRequest/HttpClient behaviour unexpected
and
http://www.telerik.com/forums/is-it-possible-to-not-buffer-requests
Using wireshark I can see the chunks.
After hours wasted on this issue, it seams that this a HttpClient implementation issue.
So if you want stream content to a server (and report progress) the best alternative is to use a StreamContent and decorate the reads to report the progress.
NOTE: This is true to the 4.3.1 version of System.Net.Http and version 2.2.29 Microsoft.Net.Http nugget packages
This is a follow up to a question I had asked previously that was closed for being too broad.Previous Question
In that question I explained that I needed upload a large file (1-3GB) to the database by storing chunks as individual rows. I did this by overriding the MultipartFormDataStreamProvider.GetStream method. That method returned a custom stream that wrote the buffered chunks to the database.
The problem is that the overriden GetStream method is writing the entire request to the database (including the headers). It is successfully writing that data while keeping the Memory levels flat but when I download the file, in addition to the file contents, it's returning all the header information in the downloaded file contents so the file can't be opened.
Is there a way to, in the overriden GetStream method, write just the contents of the file to the database without writing the headers?
API
[HttpPost]
[Route("file")]
[ValidateMimeMultipartContentFilter]
public Task<HttpResponseMessage> PostFormData()
{
var provider = new CustomMultipartFormDataStreamProvider();
// Read the form data and return an async task.
var task = Request.Content.ReadAsMultipartAsync(provider).ContinueWith<HttpResponseMessage>(t =>
{
if (t.IsFaulted || t.IsCanceled)
{
Request.CreateErrorResponse(HttpStatusCode.InternalServerError, t.Exception);
}
return Request.CreateResponse(HttpStatusCode.OK);
});
return task;
}
[HttpGet]
[Route("file/{id}")]
public async Task<HttpResponseMessage> GetFile(string id)
{
var result = new HttpResponseMessage()
{
Content = new PushStreamContent(async (outputStream, httpContent, transportContext) =>
{
await WriteDataChunksFromDBToStream(outputStream, httpContent, transportContext, id);
}),
StatusCode = HttpStatusCode.OK
};
result.Content.Headers.ContentType = new MediaTypeHeaderValue("application/zipx");
result.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment") { FileName = "test response.zipx" };
return result;
}
return new HttpResponseMessage(HttpStatusCode.BadRequest);
}
private async Task WriteDataChunksFromDBToStream(Stream responseStream, HttpContent httpContent, TransportContext transportContext, string fileIdentifier)
{
// PushStreamContent requires the responseStream to be closed
// for signaling it that you have finished writing the response.
using (responseStream)
{
using (var myConn = new SqlConnection(System.Configuration.ConfigurationManager.ConnectionStrings["TestDB"].ConnectionString))
{
await myConn.OpenAsync();
using (var myCmd = new SqlCommand("ReadAttachmentChunks", myConn))
{
myCmd.CommandType = System.Data.CommandType.StoredProcedure;
var fileName = new SqlParameter("#Identifier", fileIdentifier);
myCmd.Parameters.Add(fileName);
// Read data back from db in async call to avoid OutOfMemoryException when sending file back to user
using (var reader = await myCmd.ExecuteReaderAsync(CommandBehavior.SequentialAccess))
{
while (await reader.ReadAsync())
{
if (!(await reader.IsDBNullAsync(3)))
{
using (var data = reader.GetStream(3))
{
// Asynchronously copy the stream from the server to the response stream
await data.CopyToAsync(responseStream);
}
}
}
}
}
}
}// close response stream
}
Custom MultipartFormDataStreamProvider GetStream method implementation
public override Stream GetStream(HttpContent parent, HttpContentHeaders headers)
{
// For form data, Content-Disposition header is a requirement
ContentDispositionHeaderValue contentDisposition = headers.ContentDisposition;
if (contentDisposition != null)
{
// If we have a file name then write contents out to AWS stream. Otherwise just write to MemoryStream
if (!String.IsNullOrEmpty(contentDisposition.FileName))
{
var identifier = Guid.NewGuid().ToString();
var fileName = contentDisposition.FileName;// GetLocalFileName(headers);
if (fileName.Contains("\\"))
{
fileName = fileName.Substring(fileName.LastIndexOf("\\") + 1).Replace("\"", "");
}
// We won't post process files as form data
_isFormData.Add(false);
var stream = new CustomSqlStream();
stream.Filename = fileName;
stream.Identifier = identifier;
stream.ContentType = headers.ContentType.MediaType;
stream.Description = (_formData.AllKeys.Count() > 0 && _formData["description"] != null) ? _formData["description"] : "";
return stream;
//return new CustomSqlStream(contentDisposition.Name);
}
// We will post process this as form data
_isFormData.Add(true);
// If no filename parameter was found in the Content-Disposition header then return a memory stream.
return new MemoryStream();
}
throw new InvalidOperationException("Did not find required 'Content-Disposition' header field in MIME multipart body part..");
#endregion
}
Implemented Write method of Stream called by CustomSqlStream
public override void Write(byte[] buffer, int offset, int count)
{
//write buffer to database
using (var myConn = new SqlConnection(System.Configuration.ConfigurationManager.ConnectionStrings["TestDB"].ConnectionString)) {
using (var myCmd = new SqlCommand("WriteAttachmentChunk", myConn)) {
myCmd.CommandType = System.Data.CommandType.StoredProcedure;
var pContent = new SqlParameter("#Content", buffer);
myCmd.Parameters.Add(pContent);
myConn.Open();
myCmd.ExecuteNonQuery();
if (myConn.State == System.Data.ConnectionState.Open)
{
myConn.Close();
}
}
}
((ManualResetEvent)_dataAddedEvent).Set();
}
The "ReadAttachmentChunks" stored procedure gets the rows respective to the file from the db ordered by the time they are inserted into the database. So, the way the code works is it pulls those chunks back and then async writes it back to the PushStreamContent to go back to the user.
So my question is:
Is there a way to write ONLY the content of the file being uploaded as opposed to the headers in addition to the content?
Any help would be greatly appreciated. Thank you.
I finally figured it out. I over-complicated the write process which brought about most of the struggle. Here is my solution to my initial issue:
To keep .net from buffering the file in memory (so that you can handle large file uploads), you first need to override the WebHostBufferPolicySelector so that it doesnt buffer the input stream for your controller and then replace the BufferPolicy Selector.
public class NoBufferPolicySelector : WebHostBufferPolicySelector
{
public override bool UseBufferedInputStream(object hostContext)
{
var context = hostContext as HttpContextBase;
if (context != null)
{
if (context.Request.RequestContext.RouteData.Values["controller"] != null)
{
if (string.Equals(context.Request.RequestContext.RouteData.Values["controller"].ToString(), "upload", StringComparison.InvariantCultureIgnoreCase))
return false;
}
}
return true;
}
public override bool UseBufferedOutputStream(HttpResponseMessage response)
{
return base.UseBufferedOutputStream(response);
}
}
then for replacing the BufferPolicy Selector
GlobalConfiguration.Configuration.Services.Replace(typeof(IHostBufferPolicySelector), new NoBufferPolicySelector());
Then to avoid the default behavior of having the file stream written to disk, you need to provide a stream provider that will write to the database instead. To do this you inherit MultipartStreamProvider and override the GetStream method to return the stream that will write to your database.
public override Stream GetStream(HttpContent parent, HttpContentHeaders headers)
{
// For form data, Content-Disposition header is a requirement
ContentDispositionHeaderValue contentDisposition = headers.ContentDisposition;
if (contentDisposition != null && !String.IsNullOrEmpty(contentDisposition.FileName))
{
// We won't post process files as form data
_isFormData.Add(false);
//create unique identifier for this file upload
var identifier = Guid.NewGuid();
var fileName = contentDisposition.FileName;
var boundaryObj = parent.Headers.ContentType.Parameters.SingleOrDefault(a => a.Name == "boundary");
var boundary = (boundaryObj != null) ? boundaryObj.Value : "";
if (fileName.Contains("\\"))
{
fileName = fileName.Substring(fileName.LastIndexOf("\\") + 1).Replace("\"", "");
}
//write parent container for the file chunks that are being stored
WriteLargeFileContainer(fileName, identifier, headers.ContentType.MediaType, boundary);
//create an instance of the custom stream that will write the chunks to the database
var stream = new CustomSqlStream();
stream.Filename = fileName;
stream.FullFilename = contentDisposition.FileName.Replace("\"", "");
stream.Identifier = identifier.ToString();
stream.ContentType = headers.ContentType.MediaType;
stream.Boundary = (!string.IsNullOrEmpty(boundary)) ? boundary : "";
return stream;
}
else
{
// We will post process this as form data
_isFormData.Add(true);
// If no filename parameter was found in the Content-Disposition header then return a memory stream.
return new MemoryStream();
}
}
The custom stream you create needs to inherit Stream and override the Write method. This is where I overthought the problem and thought I needed to parse out the boundary headers that were passed via the buffer parameter. But this is actually done for you by leveraging the offset and count parameters.
public override void Write(byte[] buffer, int offset, int count)
{
//no boundary is inluded in buffer
byte[] fileData = new byte[count];
Buffer.BlockCopy(buffer, offset, fileData, 0, count);
WriteData(fileData);
}
From there, it's just plugging in the api methods for upload and download.
For upload:
public Task<HttpResponseMessage> PostFormData()
{
var provider = new CustomMultipartLargeFileStreamProvider();
// Read the form data and return an async task.
var task = Request.Content.ReadAsMultipartAsync(provider).ContinueWith<HttpResponseMessage>(t =>
{
if (t.IsFaulted || t.IsCanceled)
{
Request.CreateErrorResponse(HttpStatusCode.InternalServerError, t.Exception);
}
return Request.CreateResponse(HttpStatusCode.OK);
});
return task;
}
For download, and in order to keep the memory footprint low, I leveraged the PushStreamContent to push the chunks back to the user:
[HttpGet]
[Route("file/{id}")]
public async Task<HttpResponseMessage> GetFile(string id)
{
string mimeType = string.Empty;
string filename = string.Empty;
if (!string.IsNullOrEmpty(id))
{
//get the headers for the file being sent back to the user
using (var myConn = new SqlConnection(System.Configuration.ConfigurationManager.ConnectionStrings["PortalBetaConnectionString"].ConnectionString))
{
using (var myCmd = new SqlCommand("ReadLargeFileInfo", myConn))
{
myCmd.CommandType = System.Data.CommandType.StoredProcedure;
var pIdentifier = new SqlParameter("#Identifier", id);
myCmd.Parameters.Add(pIdentifier);
myConn.Open();
var dataReader = myCmd.ExecuteReader();
if (dataReader.HasRows)
{
while (dataReader.Read())
{
mimeType = dataReader.GetString(0);
filename = dataReader.GetString(1);
}
}
}
}
var result = new HttpResponseMessage()
{
Content = new PushStreamContent(async (outputStream, httpContent, transportContext) =>
{
//pull the data back from the db and stream the data back to the user
await WriteDataChunksFromDBToStream(outputStream, httpContent, transportContext, id);
}),
StatusCode = HttpStatusCode.OK
};
result.Content.Headers.ContentType = new MediaTypeHeaderValue(mimeType);// "application/octet-stream");
result.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment") { FileName = filename };
return result;
}
return new HttpResponseMessage(HttpStatusCode.BadRequest);
}
private async Task WriteDataChunksFromDBToStream(Stream responseStream, HttpContent httpContent, TransportContext transportContext, string fileIdentifier)
{
// PushStreamContent requires the responseStream to be closed
// for signaling it that you have finished writing the response.
using (responseStream)
{
using (var myConn = new SqlConnection(System.Configuration.ConfigurationManager.ConnectionStrings["PortalBetaConnectionString"].ConnectionString))
{
await myConn.OpenAsync();
//stored proc to pull the data back from the db
using (var myCmd = new SqlCommand("ReadAttachmentChunks", myConn))
{
myCmd.CommandType = System.Data.CommandType.StoredProcedure;
var fileName = new SqlParameter("#Identifier", fileIdentifier);
myCmd.Parameters.Add(fileName);
// The reader needs to be executed with the SequentialAccess behavior to enable network streaming
// Otherwise ReadAsync will buffer the entire BLOB into memory which can cause scalability issues or even OutOfMemoryExceptions
using (var reader = await myCmd.ExecuteReaderAsync(CommandBehavior.SequentialAccess))
{
while (await reader.ReadAsync())
{
//confirm the column that has the binary data of the file returned is not null
if (!(await reader.IsDBNullAsync(0)))
{
//read the binary data of the file into a stream
using (var data = reader.GetStream(0))
{
// Asynchronously copy the stream from the server to the response stream
await data.CopyToAsync(responseStream);
await data.FlushAsync();
}
}
}
}
}
}
}// close response stream
}
Ugh. This is nasty. With the upload, you have to make sure to
separate the headers from the content portion - you must follow the requirements RFC documents for HTTP.
Allow for chunked transfers
Of course, the content portion (unless you are transmitting text) will be binary encoded into strings.
Allow for transfers that are compressed, i.e. GZIP or DEFLATE.
Maybe - just maybe - take the encoding into account (ASCII, Unicode, UTF8, etc).
You can't really ensure that you're persisting the right info to the database without looking at all of these. For the latter items, all of your metadata as to what to do will be somewhere in the header, so it's not just a throwaway.
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!