I ran into a problem when I started to implement a global error handler for the grpc server service. The problem is that when I get a validation error, I don't want to log it, but I want to return an RpcException with information to the client, and in other Exceptions I log it and return an unhandled error. The question is why do I repeatedly get into the catch block (Exception e) after I caught the ValidationException and threw the RpcException? To the fact that it is called twice, the logic I described above breaks down for me.
The implementation is shown below:
public class ExceptionInterceptor : Interceptor
{
private readonly ILogger<ExceptionInterceptor> _logger;
public ExceptionInterceptor(ILogger<ExceptionInterceptor> logger)
{
_logger = logger;
}
public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
TRequest request,
ServerCallContext context,
UnaryServerMethod<TRequest, TResponse> continuation)
{
try
{
return await continuation(request, context);
}
catch (ValidationException e)
{
var error = string.Join(Environment.NewLine, e.Errors.Select(p => p.ErrorMessage));
throw new RpcException(new Status(StatusCode.InvalidArgument, error));
}
catch (Exception e)
{
_logger.LogError(e, "gRPC Exception");
throw;
}
}
}
Interceptor registration:
services.AddGrpc(o =>
{
o.Interceptors.Add<ExceptionInterceptor>();
});
The reason was that when we register the interceptor globally, it fires twice. If we register it for a specific service, then it works once.The answer is:
services.AddGrpc()
.AddServiceOptions<MyService>(o =>
{
o.Interceptors.Add<ExceptionInterceptor>();
});
Related
Should catching exceptions be part of the business logic such as the Service layer, or should they be caught in the controllers' methods?
For example:
Controller UpdateUser method
[HttpPut]
[Route("{id}")]
[ProducesResponseType(200)]
[ProducesResponseType(404)]
public async Task<ActionResult<UserDto>> UpdateUserInfo(int id, UserDto userRequest)
{
try
{
var user = _userMapper.ConvertToEntity(userRequest);
var updatedUser = await _userService.UpdateAsync(user, id);
var result = _userMapper.ConvertToUserDto(updatedUser);
return Ok(result);
}
catch (Exception ex)
{
_logger.LogError("Exception caught attempting to update user - Type: {ex}", ex.GetType());
_logger.LogError("Message: {ex}", ex.Message);
return StatusCode(500, ex.Message);
}
}
The Service Layer
public async Task<User> UpdateAsync(User user, int id)
{
await _repository.UpdateAsync(user, id);
return user;
}
So, should the exceptions be caught in the service layer or the controller? Or is it subjective?
It's dependent on the business of your application. maybe in your service you should use a try/catch block to adding a log or do anything when exception occurred. but usually I use a global try/catch in a middleware to get exception and send correct response to the client.
public class AdvancedExceptionHandler
{
private readonly RequestDelegate _next;
private readonly ILogger<AdvancedExceptionHandler> _logger;
private readonly IWebHostEnvironment _env;
public AdvancedExceptionHandler(RequestDelegate next, ILogger<AdvancedExceptionHandler> logger, IWebHostEnvironment env)
{
_next = next;
_logger = logger;
_env = env;
}
public async Task Invoke(HttpContext context)
{
string message = null;
HttpStatusCode httpStatusCode = HttpStatusCode.InternalServerError;
try
{
await _next(context);
}
catch (Exception ex)
{
_logger.LogError(ex.Message, ex);
if (_env.IsDevelopment())
{
var dic = new Dictionary<string, string>
{
["StackTrace"] = ex.StackTrace,
["Exception"] = ex.Message
};
message = JsonConvert.SerializeObject(dic);
}
else
{
message = "an error has occurred";
}
await WriteToReponseAsync();
}
async Task WriteToReponseAsync()
{
if (context.Response.HasStarted)
throw new InvalidOperationException("The response has already started");
var exceptionResult = new ExceptionResult(message, (int)httpStatusCode);
var result = JsonConvert.SerializeObject(exceptionResult);
context.Response.StatusCode = (int)httpStatusCode;
context.Response.ContentType = "application/json";
await context.Response.WriteAsync(result);
}
}
}
ExceptionResutl class:
public class ExceptionResult
{
public ExceptionResult(string message, int statusCode)
{
this.Message = message;
this.StatusCode = statusCode;
}
public string Message { get; set; }
public int StatusCode { get; set; }
}
public static class ExceptionHandlerMiddlewareExtension
{
public static void UseAdvancedExceptionHandler(this IApplicationBuilder app)
{
app.UseMiddleware<AdvancedExceptionHandler>();
}
}
Then adding middleware in Configure method
public void Configure(IApplicationBuilder app)
{
app.UseAdvancedExceptionHandler();//<--NOTE THIS
}
I don't use try/catch block in controllers. (my opinion)
Catching exceptions in your controller will quickly start to violate some clean code principles, like DRY.
If I understand correctly, the example you have written is that you want to log some errors in case any exceptions are thrown in your code. This is reasonable, but if you begin to add more endpoints, you'll notice you have the same try/catch in all your controller methods. The best way to refactor this is to use a middleware that will catch the exception and map it to a response that you want.
Over time as you begin to update your application to have more features you may have a situation where multiple endpoints are throwing similar errors and you want it to be handled in a similar way. For example, in your example, if the user doesn't exist, the application (in your service layer) may throw an UserNotFoundException, and you may have some other endpoints which can throw the same error, too.
You could create another middleware to handle this or even extend your existing middleware.
One of the better approaches I've seen over the years is to use this library https://github.com/khellang/Middleware/tree/master/src/ProblemDetails to handle the boiler plate for you.
I'm calling UseExceptionHandler in order to handle errors. This works fine, but not for errors thrown by other (subsequently registered) middleware.
The middleware which exceptions I need to handle is TransactionMiddleware. What it does is to save any changes to the database after a successfully completed call to an action. To be clear - it doesn't just commit a transaction, but also runs all the insert:s/update:s etc. This might fail, for example due to database constraints. (There are also other reasons not to complete the transaction, but they are not included here. Just mentioning that to explain that making the database calls earlier and reducing the TransactionMiddleware to simply commiting won't do the trick.)
Is there a way to NOT start the response before this middleware has run its full course?
My Program.cs
var builder = WebApplication.CreateBuilder(args);
new Startup(builder.Configuration).ConfigureServices(builder.Services);
app.UseExceptionHandler(errorApp =>
{
errorApp.Run(async context =>
{
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
// await ExcludedCode()...
});
});
app.UseSwagger()
.UseSwaggerUI();
app.UseRouting()
.UseCors()
.UseAuthentication()
.UseAuthorization()
.UseMiddleware<LanguageMiddleWare>()
.UseMiddleware<TransactionMiddleWare>()
.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
app.Run();
My (simplified) TransactionMiddleWare-class
public class TransactionMiddleWare
{
private readonly RequestDelegate next;
public TransactionMiddleWare(RequestDelegate next)
{
this.next = next;
}
public async Task Invoke(HttpContext context, IDataContext dataContext)
{
try
{
await next(context);
}
catch (PartialExecutionException)
{
this.Commit(context, dataContext);
throw;
}
this.Commit(context, dataContext);
}
private void Commit(HttpContext context, IDataContext dataContext)
{
if (this.ShouldTransactionCommited(context))
dataContext.SaveChanges();
else
throw new Exception("Exception example for clarity.");
}
private bool ShouldTransactionBeCommited(HttpContext context)
{
return true; // Actual code omitted for brevity.
}
}
Example of how my controllers return data (no special stuff):
[ApiController]
[Route("advertisment")]
public class AdvertismentController : ControllerBase
{
private readonly IAdvertismentService advertismentService;
private NLog.ILogger log;
public AdvertismentController(
IAdvertismentService advertismentService)
{
this.log = LogManager.GetCurrentClassLogger();
this.advertismentService = advertismentService;
}
[HttpPost]
public Result<Guid> Create([FromForm] CreateAdvertismentMultipartFormModel request)
{
var id = this.advertismentService.Create(request);
return new Result<Guid> { Data = id };
}
}
Here is what I ended up with. A change in the TransactionMiddleWare class:
public async Task Invoke(HttpContext context, IDataContext dataContext)
{
context.Response.OnStarting(state => {
this.Commit(context, dataContext);
return Task.CompletedTask;
}, context);
try
{
await next(context);
this.Commit(context, dataContext);
}
catch (PartialExecutionException)
{
this.Commit(context, dataContext);
throw;
}
}
That way it will be run and any exception will occur while it's still possible to modify the output and produce an error message.
I have an API as follows(Mock API), where I am pushing the exception in the catch block to a Central logging Portal. Again in the Exception Filter I am phrasing the Exception Message & throwing the message to User.
[customeFilter]
Public asyc Task<IactionResult> DoTask()
{
try
{
//sample Code
}
catch(exception ex)
{
_log.LogError(ex);
}
public class customeFilterAttribute : ExceptionFilterAttribute
{
public override void OnException(ExceptionContext context)
{
HttpStatusCode? httpErrorCode = HttpStatusCode.BadRequest;
if (context.Exception is DivideByZeroException dvd)
{
context.Result = new ObjectResult(dvd.Data)
{
StatusCode = (int?)HttpStatusCode.InternalServerError,
Value = "There is an issue with the data"
};
context.ExceptionHandled = true;
}
}
}
Issue: I want to keep the same Guid to both systems, i.e., to the central Logging & to the User via Exception Filter. I tried some approach to pass the information to Exception Filter, but didn't have any success with that. Even tried to find any unique identifier in the Exception object, but didn't find any.
Note:
I have a thought about Pushing the exception Log in the Exception Filter instead of Catch block. But I don't want to go with Static Class/Methods.
Do you want to track your user and his/her corresponding exception?
If you wan't my opinion, you don't need to generate a guid for this goal. You can use _httpContextAccessor.HttpContext.TraceIdentifier
Just inject your IHttpContextAccessor inside the filter and use TraceIdentifier.
For more clarification you can set that trace identifier in header of response you can do something like this:
public class CustomFilterAttribute : ExceptionFilterAttribute
{
private readonly IHttpContextAccessor _httpContextAccessor;
public CustomFilterAttribute(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public override void OnException(ExceptionContext context)
{
HttpStatusCode? httpErrorCode = HttpStatusCode.BadRequest;
if (context.Exception?.InnerException is DivideByZeroException dvd)
{
string uid = _httpContextAccessor.HttpContext.TraceIdentifier;
//logger.log(uid + exception).
_httpContextAccessor.HttpContext.Response.Headers.Add("tracking-uid", uid);
context.Result = new ObjectResult(dvd.Data)
{
StatusCode = (int?)HttpStatusCode.InternalServerError,
Value = "There is an issue with the data"
};
context.ExceptionHandled = true;
}
}
}
I want to be in control of the details of all Exceptions that occured in my web application. I want to add custom data to the exception. I also want to add more info if in debug mode. I do want to pass this as a JSON format to the user.
To do this, I want to throw an exception with an custom error code, and pass the innerexception for debug purposes.
In my startup.cs:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseMiddleware(typeof(ErrorHandlingMiddleware)); // Error handling middlware
....
In my service I throw an Exception:
catch (Exception e)
{
throw new Exception("E18", e.InnerException);
}
When I debug this. I can see e.InnerException is filled with data.
Here comes the magic... well... sort of. This is the middleware:
public class ErrorHandlingMiddleware
{
static Dictionary<string, APIMessageDetails> responseMessageCodes = new Dictionary<string, APIMessageDetails>
{
...
{"E18", new APIMessageDetails {responseMessage = "An unknown error occured.", httpStatusCode = StatusCodes.Status500InternalServerError}},
...
}
private readonly RequestDelegate next;
public ErrorHandlingMiddleware(RequestDelegate next)
{
this.next = next;
}
public async Task Invoke(HttpContext context)
{
try {
await next(context);
}
catch (Exception ex)
{
await HandleExceptionAsync(context, ex);
}
}
private static Task HandleExceptionAsync(HttpContext context, Exception ex)
{
string errorCode = ex.Message;
APIMessageDetails result;
APIMessage apiMessage = new APIMessage();
if (errorCode != null)
{
if (responseMessageCodes.TryGetValue(errorCode, out result))
{
apiMessage.responseMessageCode = errorCode;
apiMessage.messageDetails = result;
#if DEBUG
apiMessage.messageDetails.exception = ex;
#endif
}
}
var jsonResult = JsonConvert.SerializeObject(apiMessage);
context.Response.ContentType = "application/json";
context.Response.StatusCode = apiMessage.messageDetails.httpStatusCode;
return context.Response.WriteAsync(jsonResult);
}
}
When I debug, I can see that the exception that is catched in the middleware, does contain the E18, but the innerException is null. I do not understand why that is; it is passed to the Exception that is thrown...
I hope someone could help me out here.
Oeps... seems like everything was okay whit the code above. Seems there is some middleware that causes double requests...
Have to figure that out, but by disabling the middleware the innerException has been filled.
Thanks all.
In Framework WebAPI 2, I have a controller that looks like this:
[Route("create-license/{licenseKey}")]
public async Task<LicenseDetails> CreateLicenseAsync(string licenseKey, CreateLicenseRequest license)
{
try
{
// ... controller-y stuff
return await _service.DoSomethingAsync(license).ConfigureAwait(false);
}
catch (Exception e)
{
_logger.Error(e);
const string msg = "Unable to PUT license creation request";
throw new HttpResponseException(HttpStatusCode.InternalServerError, msg);
}
}
Sure enough, I get back a 500 error with the message.
How can I do something similar in ASP.NET Core Web API?
HttpRequestException doesn't seem to exist. I would prefer to continue returning the object instead of HttpRequestMessage.
What about something like this. Create a middleware where you will expose certain exception messages:
public class ExceptionMiddleware
{
private readonly RequestDelegate _next;
public ExceptionMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception ex)
{
context.Response.ContentType = "text/plain";
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
if (ex is ApplicationException)
{
await context.Response.WriteAsync(ex.Message);
}
}
}
}
Use it in your app:
app.UseMiddleware<ExceptionMiddleware>();
app.UseMvc();
And then in your action throw the exception:
[Route("create-license/{licenseKey}")]
public async Task<LicenseDetails> CreateLicenseAsync(string licenseKey, CreateLicenseRequest license)
{
try
{
// ... controller-y stuff
return await _service.DoSomethingAsync(license).ConfigureAwait(false);
}
catch (Exception e)
{
_logger.Error(e);
const string msg = "Unable to PUT license creation request";
throw new ApplicationException(msg);
}
}
A better approach is to return an IActionResult. That way you dont have to throw an exception around. Like this:
[Route("create-license/{licenseKey}")]
public async Task<IActionResult> CreateLicenseAsync(string licenseKey, CreateLicenseRequest license)
{
try
{
// ... controller-y stuff
return Ok(await _service.DoSomethingAsync(license).ConfigureAwait(false));
}
catch (Exception e)
{
_logger.Error(e);
const string msg = "Unable to PUT license creation request";
return StatusCode((int)HttpStatusCode.InternalServerError, msg)
}
}
It's better not to catch all exceptions in every action. Just catch exceptions you need to react specifically and catch (and wrap to HttpResponse) all the rest in Middleware.