I'm trying to log the response body of an http request in Cloudwatch logs. It works fine locally but the response body is always empty when deploying the lambda to AWS. Below is the code for my middleware and the associated Cloudwatch log that appears. Notice the ResponseBody element is empty.
public class LoggingTestMiddleware
{
private readonly RequestDelegate _next;
private readonly RequestResponseLoggerOption _options;
private readonly IRequestResponseLogger _logger;
public LoggingTestMiddleware
(RequestDelegate next, IOptions<RequestResponseLoggerOption> options,
IRequestResponseLogger logger)
{
_next = next;
_options = options.Value;
_logger = logger;
}
public async Task InvokeAsync(HttpContext httpContext,
IRequestResponseLogModelCreator logCreator)
{
RequestResponseLogModel log = logCreator.LogModel;
// Middleware is enabled only when the
// EnableRequestResponseLogging config value is set.
if (_options == null || !_options.IsEnabled)
{
await _next(httpContext);
return;
}
log.RequestDateTimeUtc = DateTime.UtcNow;
HttpRequest request = httpContext.Request;
/*log*/
log.LogId = Guid.NewGuid().ToString();
log.TraceId = httpContext.TraceIdentifier;
var ip = request.HttpContext.Connection.RemoteIpAddress;
log.ClientIp = ip == null ? null : ip.ToString();
log.Node = _options.Name;
/*request*/
log.RequestMethod = request.Method;
log.RequestPath = request.Path;
log.RequestQuery = request.QueryString.ToString();
log.RequestQueries = FormatQueries(request.QueryString.ToString());
log.RequestHeaders = FormatHeaders(request.Headers);
log.RequestBody = await ReadBodyFromRequest(request);
log.RequestScheme = request.Scheme;
log.RequestHost = request.Host.ToString();
log.RequestContentType = request.ContentType;
// Temporarily replace the HttpResponseStream,
// which is a write-only stream, with a MemoryStream to capture
// its value in-flight.
HttpResponse response = httpContext.Response;
var originalResponseBody = response.Body;
using var newResponseBody = new MemoryStream();
response.Body = newResponseBody;
// Call the next middleware in the pipeline
try
{
await _next(httpContext);
}
catch (Exception exception)
{
/*exception: but was not managed at app.UseExceptionHandler()
or by any middleware*/
LogError(log, exception);
}
newResponseBody.Seek(0, SeekOrigin.Begin);
var responseBodyText =
await new StreamReader(response.Body).ReadToEndAsync();
(string, bool) test = (Encoding.UTF8.GetString(((MemoryStream)newResponseBody).ToArray()), false);
var responseBodyTest = test.Item1;
log.ResponseBody = responseBodyTest;
newResponseBody.Seek(0, SeekOrigin.Begin);
await newResponseBody.CopyToAsync(originalResponseBody);
/*response*/
log.ResponseContentType = response.ContentType;
log.ResponseStatus = response.StatusCode.ToString();
log.ResponseHeaders = FormatHeaders(response.Headers);
//log.ResponseBody = responseBodyText;
log.ResponseDateTimeUtc = DateTime.UtcNow;
/*exception: but was managed at app.UseExceptionHandler()
or by any middleware*/
var contextFeature =
httpContext.Features.Get<IExceptionHandlerPathFeature>();
if (contextFeature != null && contextFeature.Error != null)
{
Exception exception = contextFeature.Error;
LogError(log, exception);
}
//var jsonString = logCreator.LogString(); /*log json*/
_logger.Log(logCreator);
}
private void LogError(RequestResponseLogModel log, Exception exception)
{
log.ExceptionMessage = exception.Message;
log.ExceptionStackTrace = exception.StackTrace;
}
private Dictionary<string, string> FormatHeaders(IHeaderDictionary headers)
{
Dictionary<string, string> pairs = new Dictionary<string, string>();
foreach (var header in headers)
{
pairs.Add(header.Key, header.Value);
}
return pairs;
}
private List<KeyValuePair<string, string>> FormatQueries(string queryString)
{
List<KeyValuePair<string, string>> pairs =
new List<KeyValuePair<string, string>>();
string key, value;
foreach (var query in queryString.TrimStart('?').Split("&"))
{
var items = query.Split("=");
key = items.Count() >= 1 ? items[0] : string.Empty;
value = items.Count() >= 2 ? items[1] : string.Empty;
if (!String.IsNullOrEmpty(key))
{
pairs.Add(new KeyValuePair<string, string>(key, value));
}
}
return pairs;
}
private async Task<string> ReadBodyFromRequest(HttpRequest request)
{
// Ensure the request's body can be read multiple times
// (for the next middlewares in the pipeline).
request.EnableBuffering();
using var streamReader = new StreamReader(request.Body, leaveOpen: true);
var requestBody = await streamReader.ReadToEndAsync();
// Reset the request's body stream position for
// next middleware in the pipeline.
request.Body.Position = 0;
return requestBody;
}
}
I have the middleware registered in Startup.cs as follows:
public void Configure(IApplicationBuilder app)
{
app.UseHsts();
app.Use((context, next) =>
{
context.Response.Headers.Add("X-Content-Type-Options", "nosniff");
return next.Invoke();
});
app.UseErrorHandler();
app.UseXRay(AssemblyName);
app.UseRouting();
app.UseOpenApi(settings =>
{
settings.Path = "/docs/{documentName}/swagger.json";
});
app.UseStaticFiles();
app.UseSwaggerUi(x =>
{
x.Path = "/docs";
x.DocumentTitle = "Audere API Documentation";
x.DocumentPath = "/docs/{documentName}/swagger.json";
x.DocExpansion = "list";
x.CustomStylesheetPath = "/docs/styles.css";
x.PersistAuthorization = !this.IsLambda;
x.TagsSorter = "alpha";
x.OperationsSorter = "method";
x.AdditionalSettings["filter"] = true;
x.AdditionalSettings["deepLinking"] = true;
x.AdditionalSettings["displayRequestDuration"] = true;
});
app.UseApiGatewayAuthorizerAuthentication();
app.UseAuthorization();
app.UseLambdaRequestLogging();
app.UseMiddleware<LoggingTestMiddleware>();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapHealthChecks();
endpoints.MapGet("/", x =>
{
x.Response.StatusCode = StatusCodes.Status302Found;
x.Response.Headers.Location = "/docs/index.html";
return Task.CompletedTask;
});
});
}
I have tried several different implementations of the middleware but so far have had no success.They show the request body but the response body is empty.

Was able to figure out a solution to this using the code below. Apparently, it is necessary to swap out the response.Body with a new MemoryStream before using it.