I have created a generic try/catch method on base API on a net core 2.2 project, and I am not sure about perfomance of this generic method. Is this a good way to do it?
This is on base api:
protected async Task<IActionResult> TryReturnOk<TReturn>(Func<Task<TReturn>> function)
{
try
{
var result = await function();
return Ok(result);
}
catch (Exception ex)
{
_fileLogger.LogError(ex.Message);
_fileLogger.LogError(ex.StackTrace);
return BadRequest(ex);
}
}
And it is used on post method in the api-s like:
public async Task<IActionResult> Post([FromBody] LogViewModel log)
{
return await TryReturnOk(() => _writeLogService.WriteLog(log));
}
Instead of cluttering up all your controllers, I would centralized logging to middleware like below.
400 Bad Request should be used when e.g. request model is not valid. When an exception is thrown, 500 is more appropriate.
public class LoggerMiddleware
{
private readonly ILogger _fileLogger;
private readonly RequestDelegate _next;
public LoggerMiddleware(RequestDelegate next, ILogger fileLogger)
{
_next = next;
_fileLogger = fileLogger;
}
public async Task Invoke(HttpContext context)
{
try
{
await _next.Invoke(context);
}
catch (Exception ex)
{
_fileLogger.LogError(ex.Message);
_fileLogger.LogError(ex.StackTrace);
context.Response.Clear();
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
}
}
}
public static class MiddlewareExtensions
{
public static IApplicationBuilder UseLoggerMiddleware(this IApplicationBuilder builder)
{
return builder.UseMiddleware<LoggerMiddleware>();
}
}
In Startup#Configure
app.UseLoggerMiddleware()
Related
I need to handle an incoming request which is of the form:
//ohif/study/1.1/series
Note the exta slash at the front
My controller signature is:
[Route("ohif/study/{studyUid}/series")]
[HttpGet]
public IActionResult GetStudy(string studyUid)
If I modify the incoming request to /ohif/study/1.1/series it works fine
however when I use //ohif/study/1.1/series, the route is not hit
Additionally I also tried: [Route("/ohif/study/{studyUid}/series")]
and [Route("//ohif/study/{studyUid}/series")]
Both fail. I unfortunately cannot change the incoming request as it is from an external application. Is there some trick to handle this route? I am working in .NET Core 3.0.
Update NOTE:
I have logging activated and I see that asp.net core is analyzing the route, I have the message:
No candidates found for the request path '//ohif/study/1.1/series'
for the logger Microsoft.AspNetCore.Routing.EndpointRoutingMiddleware
What about the middleware to handle double slash?
app.Use((context, next) =>
{
if (context.Request.Path.Value.StartsWith("//"))
{
context.Request.Path = new PathString(context.Request.Path.Value.Replace("//", "/"));
}
return next();
});
Rewrite the URL at the web server-level, e.g. for IIS, you can use the URL Rewrite Module to automatically redirect //ohif/study/1.1/series to /ohif/study/1.1/series. This isn't a job for your application.
I took Ravi's answer and fleshed out a middleware. The middleware is nice because it is encapsulated, easily testable, can inject a logger, more readable, etc.
app.UseDoubleSlashHandler();
The code and tests:
public class DoubleSlashMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<DoubleSlashMiddleware> _logger;
public DoubleSlashMiddleware(RequestDelegate next, ILogger<DoubleSlashMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
_logger.LogInformation($"Invoking {nameof(DoubleSlashMiddleware)} on {context.Request.Path}");
context.Request.Path = context.Request.Path.FixDoubleSlashes();
// Call the next delegate/middleware in the pipeline.
await _next(context);
}
}
public static class DoubleSlashMiddlewareExtensions
{
public static IApplicationBuilder UseDoubleSlashHandler(
this IApplicationBuilder builder)
{
return builder.UseMiddleware<DoubleSlashMiddleware>();
}
}
[TestClass()]
public class DoubleSlashMiddlewareTests
{
private DoubleSlashMiddleware _sut;
private ILogger<DoubleSlashMiddleware> _logger;
private bool _calledNextMiddlewareInPipeline;
[TestInitialize()]
public void TestInitialize()
{
_logger = Substitute.For<ILogger<DoubleSlashMiddleware>>();
Task Next(HttpContext _)
{
_calledNextMiddlewareInPipeline = true;
return Task.CompletedTask;
}
_sut = new DoubleSlashMiddleware(Next, _logger);
}
[TestMethod()]
public async Task InvokeAsync()
{
// Arrange
_calledNextMiddlewareInPipeline = false;
// Act
await _sut.InvokeAsync(new DefaultHttpContext());
// Assert
_logger.ReceivedWithAnyArgs(1).LogInformation(null);
Assert.IsTrue(_calledNextMiddlewareInPipeline);
}
}
String method to do the replacement:
public static class RoutingHelper
{
public static PathString FixDoubleSlashes(this PathString path)
{
if (string.IsNullOrWhiteSpace(path.Value))
{
return path;
}
if (path.Value.Contains("//"))
{
return new PathString(path.Value.Replace("//", "/"));
}
return path;
}
}
[TestClass()]
public class RoutingHelperTests
{
[TestMethod()]
[DataRow(null, null)]
[DataRow("", "")]
[DataRow("/connect/token", "/connect/token")]
[DataRow("//connect/token", "/connect/token")]
[DataRow("/connect//token", "/connect/token")]
[DataRow("//connect//token", "/connect/token")]
[DataRow("/connect///token", "/connect/token")]
public void FixDoubleSlashes(string input, string expected)
{
// Arrange
var path = new PathString(input);
// Act
var actual = path.FixDoubleSlashes();
// Assert
Assert.AreEqual(expected, actual.Value);
}
}
I am trying to create custom exception handling middlware. similar to -> app.UseExceptionHandler("/Error"). I will log the error in middleware and then i want invoke Error page.
The problem is, when i do redirect then IExceptionHandlerFeature object from context.Features.Get() is NULL.
Seems that context exceptions are cleared when Razorpage is executed. In the original middleware it somehow works.
public class ExceptionMiddleware
{
private readonly RequestDelegate _next;
private readonly ILoggerFactory _loggerFactory;
public ExceptionMiddleware(RequestDelegate next, ILoggerFactory loggerFactory)
{
_loggerFactory = loggerFactory;
_next = next;
}
public async Task InvokeAsync(HttpContext httpContext)
{
try
{
await _next(httpContext);
}
catch (Exception ex)
{
var logger = _loggerFactory.CreateLogger("Serilog Global exception logger");
logger.LogError($"Something went wrong: {ex}");
await HandleExceptionAsync(httpContext, ex);
}
}
private async Task HandleExceptionAsync(HttpContext context, Exception exception)
{
context.Response.ContentType = "application/json";
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
context.Response.Redirect("/ErrorHandling/Error");
await Task.CompletedTask;
}
}
Here is my Razor Error Page model
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public class ErrorModel : PageModel
{
private ILogger<ErrorModel> _logger;
public string RequestId { get; set; }
public string ExceptionPath { get; set; }
public string ExceptionMessage { get; set; }
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
public ErrorModel(ILogger<ErrorModel> logger)
{
_logger = logger;
}
public void OnGet(string executionPath)
{
var httpCtx = HttpContext;
var exceptionDetails = httpCtx.Features.Get<IExceptionHandlerPathFeature>();
if (exceptionDetails != null)
{
_logger.LogError(httpCtx.Response.StatusCode, exceptionDetails.Error, exceptionDetails.Error.Message);
//Add data for view
ExceptionPath = exceptionDetails.Path;
ExceptionMessage = "An unexpected fault happened. Try again later.";
}
RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
}
}
After redirection httpCtx.Features.Get() should return IExceptionHandlerPathFeature object, but it returns NULL
From the source code of ExceptionHandlerMiddleware and ExceptionHandlerExtensions, you need to use rewrite instead of redirect.
Below is simplified code of custom ExceptionMiddleware
public class ExceptionMiddleware
{
private readonly RequestDelegate _next;
private readonly ILoggerFactory _loggerFactory;
public ExceptionMiddleware(
RequestDelegate next,
ILoggerFactory loggerFactory
)
{
_next = next;
_loggerFactory = loggerFactory;
}
public async Task InvokeAsync(HttpContext httpContext)
{
try
{
await _next(httpContext);
}
catch (Exception ex)
{
var logger = _loggerFactory.CreateLogger("Serilog Global exception logger");
logger.LogError($"Something went wrong: {ex}");
await HandleExceptionAsync(httpContext, ex);
}
}
private async Task HandleExceptionAsync(HttpContext context, Exception ex)
{
PathString originalPath = context.Request.Path;
context.Request.Path = "/Error";
try
{
var exceptionHandlerFeature = new ExceptionHandlerFeature()
{
Error = ex,
Path = originalPath.Value,
};
context.Features.Set<IExceptionHandlerFeature>(exceptionHandlerFeature);
context.Features.Set<IExceptionHandlerPathFeature>(exceptionHandlerFeature);
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
await _next(context);
return;
}
catch (Exception ex2)
{
}
finally
{
context.Request.Path = originalPath;
}
}
}
I initialize logging in program.cs:
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseApplicationInsights()
.UseStartup<Startup>();
And later I have some global exception handling in Startup.cs:
public static void ConfigureExceptionHandler(this IApplicationBuilder app)
{
app.UseExceptionHandler(appError =>
{
appError.Run(async context =>
{
context.Response.StatusCode = 200;
});
});
}
What I've noticed, code from my ITelemetryProcessor is executed before the one from app.UseExceptionHandler.
As the result, handled exceptions are being logged to Application Insights. How can I prevent it?
Ok so I found a decent workaround. I used custom middleware for changing response codes, so the exception doesn't pop up to insights, as long as it's handled in the middleware. Example:
public class ExceptionHandlingMiddleware
{
private readonly RequestDelegate _next;
public ExceptionHandlingMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext httpContext)
{
try
{
await _next(httpContext);
}
catch (Exception ex)
{
httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;
}
}
}
I have setup Serilog to log to MSSql using:
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.MinimumLevel.Override("Microsoft", LogEventLevel.Information)
.MinimumLevel.Override("System", LogEventLevel.Information)
.MinimumLevel.Override("Microsoft.AspNetCore.Authentication", LogEventLevel.Information)
.Enrich.FromLogContext()
.WriteTo.Async(x => x.MSSqlServer(logConntectionString, tableName, LogEventLevel.Warning, autoCreateSqlTable: false, columnOptions: columnOptions))
.CreateLogger();
Additionally I have added added a SerilogMiddleware in the pipeline that successfully adds LogContext from the HttpContext.
In a test controller, I have these 2 test methods:
public class TestController : ControllerBase
{
[HttpGet, Route("test")]
public IActionResult Get() {
try
{
string[] sar = new string[0];
var errorgenerator = sar[2]; // Trigger exception
}
catch (Exception ex)
{
Log.Error(ex, "Caught Exception");
return StatusCode(500, "Custom 500 Error");
}
return Ok();
}
[HttpGet, Route("test2")]
public IActionResult Get2() {
string[] sar = new string[0];
var errorgenerator = sar[2];// Trigger exception
return Ok();
}
}
The first method is not DRY, and so I would like to handle global/uncaught exceptions such as method 2.
What I have from here is:
public class GloablExceptionFilter : ActionFilterAttribute, IExceptionFilter
{
public void OnException(ExceptionContext context)
{
var httpContext = context.HttpContext; // This does not appear to have the actual HttpContext
Log.Error(context.Exception, "Unhandled Exception");
}
}
Problem is, my middleware that otherwise worked no longer does.. It does not edit the response body, etc... Further, when I access ExceptionContext's context.HttpContext, it does not contain the actual HttpContext when triggered from inside a controller method such as above.
How do I inject or share HttpContext and or LogContext with this Filter?
If thats not possible, how do I accomplish logging exceptions, while being DRY, and having context when its available?
Update 1: Current Middleware
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddSerilog();
app.UseAuthentication();
// Logging Middleware is just after Authentication, to have access to
// user IsAuthorized, claims, etc..
app.UseMiddleware<SerilogMiddleware>();
app.UseCors("CORSPolicy");
app.UseMvc();
}
In the middleware itself:
public class SerilogMiddleware
{
readonly RequestDelegate _next;
public SerilogMiddleware(RequestDelegate next)
{
if (next == null) throw new ArgumentNullException(nameof(next));
_next = next;
}
public async Task Invoke(HttpContext httpContext)
{
// Do logging stuff with Request..
await _next(httpContext);
// Do logging stuff with Response but..
// This point is never reached, when exception is unhandled.
}
}
Based on code snippet you are not catching the exception when you pass the context down the pipeline.
If you do not catch/handle the exception within the middleware then it wont reach your code after calling down stream.
public class SerilogMiddleware {
readonly RequestDelegate _next;
public SerilogMiddleware(RequestDelegate next) {
if (next == null) throw new ArgumentNullException(nameof(next));
_next = next;
}
public async Task Invoke(HttpContext httpContext) {
// Do logging stuff with Request..
try {
await _next(httpContext);
} catch(Exception ex) {
try {
//Do exception specific logging
// if you don't want to rethrow the original exception
// then call return:
// return;
} catch (Exception loggingException) {
//custom
}
// Otherwise re -throw the original exception
throw;
}
// Do logging stuff with Response
}
}
The above will re-throw the original error after logging it so that the other handler in the pipeline will catch it and do the out of the box handling.
I've created a HandlerMiddleware-class, which executes a handler for a specific URL.
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
namespace FailTest
{
public interface IHttpHandler
{
bool IsReusable { get; }
void ProcessRequest(HttpContext context);
}
public abstract class HandlerMiddleware<T>
where T : IHttpHandler
{
private readonly RequestDelegate _next;
public HandlerMiddleware()
{ }
public HandlerMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
try
{
await SyncInvoke(context);
}
catch (System.Exception ex)
{
System.Console.WriteLine(ex.Message);
throw;
}
}
public Task SyncInvoke(HttpContext context)
{
try
{
// IHttpHandler handler = (IHttpHandler)this;
T handler = System.Activator.CreateInstance<T>();
handler.ProcessRequest(context);
}
catch (System.Exception ex)
{
System.Console.WriteLine(ex.Message);
throw;
}
return Task.CompletedTask;
}
} // End Abstract Class HandlerMiddleware
}
Now I have a class which implements a handler, e.g.
[HandlerPath("/treedata", "GET,POST")]
public class SomeFailingTask
: HandlerMiddleware<SomeFailingTask>, IHttpHandler
{
public SomeFailingTask() : this(null)
{ }
public SomeFailingTask(RequestDelegate next) : base(next)
{ }
bool IHttpHandler.IsReusable
{
get { throw new System.NotImplementedException(); }
}
void IHttpHandler.ProcessRequest(HttpContext context)
{
// Do something in DB and return result as JSON
// e.g. SQL with invalid syntax, or a missing parameter
}
}
It works fine as long as there are no errors in ProcessRequest.
But when an exception is thrown in ProcessRequest, it finishes the request with HTTP 200 OK.
What am I doing wrong ?
I get the exception, in both Invoke and SyncInvoke, but the HTTP request always finished as if everything was fine...