We are putting an angular front end on an existing asp.net c# MVC applicaiton. In our server code, we extensilvely use custom exceptions to return buisness rule errors.
Is there a best practice or slickest way to handle an exception on an mvc controller or an webApi controller (actually bullbing up from the buisness layer) and getting it across to angular and displaying it in a "user error" popup? How are folks solving this problem?
Other guys already gave great answers, but I want to elaborate my approach since I guess it will be covering both ends (frontend and server) with more details.
Here's my complete approach to error and exception handling in WebAPI + AngularJS applications.
Step 1. WebApi controllers in Server side
I have a specific DTO for communicating Validation Errors to the client, since I believe they are different from Exceptions. An exception will result in a 500 error, where a validation result should result in 400 (Bad Request) error.
So, here's my ApiValidationResult class:
public class ApiValidationResult
{
public List<ApiValidationError> Errors { get; set; }
public static ApiValidationResult Failure(string errorKey)
{
return new ApiValidationResult {Errors = new List<ApiValidationError> {new ApiValidationError(errorKey)}};
}
// You can add a bunch of utility methods here
}
public class ApiValidationError
{
public ApiValidationError()
{
}
public ApiValidationError(string errorKey)
{
ErrorKey = errorKey;
}
// More utility constructors here
public string PropertyPath { get; set; }
public string ErrorKey { get; set; }
public List<string> ErrorParameters { get; set; }
}
I always use my own base class for WebApi (and MVC) controllers, so I can use them to add handy result method, such as this:
public abstract class ExtendedApiController : ApiController
{
protected IHttpActionResult ValidationError(string error)
{
return new ValidationErrorResult(ApiValidationResult.Failure(error), this);
}
// More utility methods can go here
}
It uses a custom IHttpActionResult that I've created specifically for this purpose:
public class ValidationErrorResult : NegotiatedContentResult<ApiValidationResult>
{
public ValidationErrorResult(ApiValidationResult content, IContentNegotiator contentNegotiator, HttpRequestMessage request, IEnumerable<MediaTypeFormatter> formatters)
: base(HttpStatusCode.BadRequest, content, contentNegotiator, request, formatters)
{
}
public ValidationErrorResult(ApiValidationResult content, ApiController controller)
: base(HttpStatusCode.BadRequest, content, controller)
{
}
}
As a result, I can cleanly use codes such as this in my controller actions:
[HttpPost]
public IHttpActionResult SomeAction(SomeInput input)
{
// Do whatever...
if (resultIsValid)
{
return Ok(outputObject);
}
return ValidationResult(errorMessage);
}
Step 2. Handling unexpected exceptions
As I said, I believe that only real unhandled Exceptions should result in a 500 (Internal server error) responses.
Such unhandled exceptions are automatically converted to a 500 result by WebApi. The only thing I need to do about them, is to log them. So, I create an implementation of IExceptionLogger interface and register it like this:
GlobalConfiguration.Configuration.Services.Add(typeof(IExceptionLogger), new UnhandledExceptionLogger());
Step 3. Intercepting and showing errors in Client side
AngularJS allows intercepting all HTTP calls sent from $http service. I use this to centralize all message popups. Here's my interceptor code:
appModule.factory("errorsHttpInterceptor", [
"$q", "$rootScope", "$injector",
($q: ng.IQService, $rootScope: IAppRootScopeService, $injector) => {
return {
'responseError': rejection => {
// Maybe put the error in $rootScope and show it in UI
// Maybe use a popup
// Maybe use a 'toast'
var toastr = $injector.get('toastr');
toastr.error(...);
return $q.reject(rejection);
}
};
}
]);
You can do all sorts of things in the interceptor, such as logging debug messages, or applying key to display-string translation of error codes. You can also distinguish between 500 and 400 errors, and display different types of error messages.
I use toastr library which I think shows a nice UI and is very handy in API level.
Finally, I register the interceptor like this:
appModule.config([
'$httpProvider',
($httpProvider: ng.IHttpProvider) => {
$httpProvider.interceptors.push('errorsHttpInterceptor');
}
]);
The syntax is in TypeScript, which is very similar to JavaScript and I'm sure you can figure out what it means.
Typically, I Have been doing this kind of thing within our WEB API, returning the correct status code is key with this, this is of course completely agnostic as to which of the many front end frameworks you want to use.
public IHttpActionResult Get(DateTime? updatesAfter = null)
{
try
{
// Do something here.
return this.Ok(result);
}
catch (Exception ex) // Be more specific if you like...
{
return this.InternalServerError(ex);
throw;
}
}
The helper methods that are now shipped with Web Api v2 ApiControllers are excellent...
this.BadRequest()
this.InternalServerError()
this.Ok()
this.Unauthorized()
this.StatusCode()
this.NotFound()
Some of them (such as InternalServerError) allow you to pass an exception or message (or simply an object) as a param.
Typically as with any front end framework or library there will be a fail or error callback that you can provide when initialising the ajax call to your API method, this will be called in scenarios where error status codes are returned.
We have just finished a large MVC app with an Angular frontend.
We just let errors flow, eg any webpages just get an error has but without the stack trace (not the yellow screen, the nice Error has *** but within your master page etc)
Web API calls just return the correct HTTP status. They can include some details if you wish.
But, you dont want to lose these errors, so we just installed elmah with
Install-Package elmah
and, it JUST WORKS, errors just end up in the log, users get told something as not worked etc.. NO extra work needed
To make our UI nicer we did the following, for
Unhandled errors
MVC Pages
Just let MVC front end do its job, it will tell the user in a nice way something has gone wrong.
Angular web calls
in the .error function alert the user eg
}).error(function (data, status, headers, config) {
$scope.addAlert('danger', 'Error deleting autosave details');
}
Let errors stack, if you don't to lose an error due to an error overwriting it. addAlert just writes to an array that is data bound to on the front end.
Handled Errors
If you are handling them, then you have managed what happens, but to log these
MVC
If you wish to just log them, the elmah API has a single call for this
ErrorSignal.FromCurrentContext().Raise(exception );
If you are using error attributes then you can use this
public class FilterConfig
{
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new ElmahHandledErrorLoggerFilter());
filters.Add(new HandleErrorAttribute());
}
}
public class ElmahHandledErrorLoggerFilter : IExceptionFilter
{
public void OnException(ExceptionContext context)
{
// Log only handled exceptions, because all other will be caught by ELMAH anyway.
if (context.ExceptionHandled)
ErrorSignal.FromCurrentContext().Raise(context.Exception);
}
}
Also, for general logging to the ELMAH framework checkout https://github.com/TrueNorthIT/Elmah
Another approach is returning Content.
Here is how I do it (end-to-end). From my API:
return Content(HttpStatusCode.<statuscode>, "ResponseContent: " + "my_custom_error");
Here, HttpStatusCode. and "my_custom_error" can be the response returned from another API Layer. In that case, I simply read the response from that layer and pass it to the client side.
//If I'm getting output from another API/Layer then I pass it's output like this
var output = response.Content.ReadAsStringAsync().Result;
return Content(response.StatusCode, "ResponseContent: " + output);
For more details on HttpStatusCodes, please refer HttpStatusCode Enumeration
And in the Angular code, I read it like this:
$http({ method: methodType, url: endpoint })
.then(function (response) {
response.status; //gets you the HttpStatusCode to play with
response.data; //gets you the ReponseContent section
}, function (response) {
response.status; //gets you the HttpStatusCode
response.data; //gets you the ReponseContent section
});
Make sure while making http calls the responseType is not set to 'JSON'. Since the data returned by the API is not in JSON format at this stage.
From $http service in AngularJS, the response object has these properties:
data – {string|Object} – The response body transformed with the
transform functions.
status – {number} – HTTP status code of the response.
headers – {function([headerName])} – Header getter function.
config – {Object} – The configuration object that was used to
generate the request.
statusText – {string} – HTTP status text of the response.
For my API Controllers I return HttpResponseMessage. Please see my example below. I hope this helps.
On the response message you can also pass back your object to the front end.
WEB API
// Get all
[HttpGet]
public HttpResponseMessage Get()
{
try
{
return Request.CreateResponse(HttpStatusCode.OK, myObject);
}
catch (Exception e)
{
return Request.CreateErrorResponse(HttpStatusCode.InternalServerError, "Error Message");
}
}
Angular JS
// Get Request on Page Load
$http({
url: '/api/department/',
method: 'GET',
}).success(function (data) {
$scope.departments = data;
}).error(function (error) {
$scope.error = error;
});
HTML
<div class="alert alert-danger animate-show" role="alert" ng-show="error != null">{{ error }} </div>
<div class="alert alert-success animate-show" role="alert" ng-show="success != null">{{ success }}</div>
Related
Background
I have a web api that written with asp.net core v2.1. This is the function which exposed by my service:
[HttpPost]
[Route("submit")]
public async Task<ActionResult<int>> DoIt(CustomModel model)
{
if (!ModelState.IsValid)
return new StatusCodeResult(403); // Custom 403 will be here.
// ...
}
And this is the CustomModel:
public class CustomModel
{
[Required]
public int MagicNumber { get; set; }
}
This combination (method+model) is working fine until the client don't provide the MagicNumber to the server.
The Problem
In contrast to the old asp.net (.net framework), when model validation is failed - an automatic 403 error message is sent to the client.
I want to prevent this default behavior and give a custom error message to the user. I prefer to define a custom response like this way:
[HttpPost]
[Route("submit")]
public async Task<ActionResult<int>> Submit(CustomModel model)
{
if (!ModelState.IsValid)
return new CustomErrorCode_BadRequest();
//...
}
Things I tried
The only way that I could prevent the automatic "bad-request" response is by adding this code to the Startup.cs file
services.AddMvc(options => options.ModelValidatorProviders.Clear());
After adding this line of code, I was able to reach my if (!ModelState.IsValid) statement. Before that, the request was blocked on earlier step. Unfortenetly, the ModelState.IsValid return always true (no matter what is the input). I guess that this is because I "cleared" all of the validators - which sounds like a bad idea.
How it should be done?
Thanks!
Thanks for #Compufreak (source: https://stackoverflow.com/a/51522487/3085985) Adding this code to Starup.cs solved the problem:
services.Configure<ApiBehaviorOptions>(opt =>
{
opt.SuppressModelStateInvalidFilter = true;
});
I have a project with Attribute routing like:
[Route("home")]
public class HomeController : Controller
{
[HttpPost]
public IActionResult Post(int id)
{
}
[HttpGet]
public IActionResult Get()
{
}
}
Now I want to catch all Get/Post/Put Requests which doesn't have a specified route. So I can return an error, redirect to the home page and such stuff. Is it possible with AttributeRouting or should I use Conventional Routing in the startup? And how would the "not existing" route look there?
By default, server returns 404 HTTP Status code as the response for requests that are not handled by any middleware (attribute/convention routing is part of MVC middleware).
In general, what you always can do is to add some middleware at the beginning of the pipeline to catch all responses with 404 status code and do custom logic or change response.
In practice, you can use the existing mechanism provided by ASP.NET Core called StatusCodePagesmiddleware. You can register it directly as raw middleware by
public void Configure(IApplicationBuilder app)
{
app.UseStatusCodePages(async context =>
{
context.HttpContext.Response.ContentType = "text/plain";
await context.HttpContext.Response.WriteAsync(
"Status code page, status code: " +
context.HttpContext.Response.StatusCode);
});
//note that order of middlewares is importante
//and above should be registered as one of the first middleware and before app.UseMVC()
The middleware supports several extension methods, like the following (the difference is well explained in this article):
app.UseStatusCodePages("/error/{0}");
app.UseStatusCodePagesWithRedirects("/error/{0}");
app.UseStatusCodePagesWithReExecute("/error/{0}");
where "/error/{0}" is a routing template that could be whatever you need and it's {0} parameter will represent the error code.
For example to handle 404 errors you may add the following action
[Route("error/404")]
public IActionResult Error404()
{
// do here what you need
// return custom API response / View;
}
or general action
[Route("error/{code:int}")]
public IActionResult Error(int code)
Testing my web API (nuget package Microsoft.AspNetCoreAll 2.0.5) I run into strange issues with the model validation using annotations.
I have (for example) this controller:
[HttpPost]
public IActionResult Create([FromBody] RequestModel request)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
// create
request.Name.DoSomething();
return Created(...);
}
I defined my RequestModel as follows:
public class RequestModel
{
[Required]
public string Name {get; set};
}
My problem although I defined RequestModel.Name as [Required] it is null (if Name is not present in the json from the body. Which I thought should not happen since it is marked as [Required] and be automatically appear as ModelState error.
Given this link to the specs they use Bind(....).
So my question?
Do I have to enable it everytime or should it work out of the box or how is it intended to be used?
If I annotate it with [Required] I would assume that at least ModelState.IsValid returns false if it is not present.
Using Bind in the link seems a bit complicated for me in cases where I have multiple objects nested into each other.
Edit 1: created a MVC data validation test bed
To better visualize what I mean and so everyone can easily experiment on their own I created the small demo .NET Core MVC data validation test bed on GitHub.
You can download the code, start it with VS 2017 and try it out your own using the swagger ui.
Having this model:
public class StringTestModel2
{
[Required]
public string RequiredStringValue { get; set; }
}
And testing it with that controller:
[HttpPost("stringValidationTest2")]
[SwaggerOperation("StringValidationTest2")]
public IActionResult StringValidationTest2([FromBody] StringTestModel2 request)
{
LogRequestModel("StringValidationTest2", request);
if (!ModelState.IsValid)
{
LogBadRequest(ModelState);
return BadRequest(ModelState);
}
LogPassedModelValidation("StringValidationTest2");
return Ok(request);
}
The results are far way from expected:
Giving a null (not the string "null") is allowed and return 200 OK
Giving an int is allowed an returns 200 OK (it gets converted to a string)
Giving a double is allowed and returns 200 OK (if possible it gets converted to string, if not convertible (mixing points and semicolons return 400 Bad Request)
if you just send empty curly brackets and leave RequiredStringValue undefined it passes and returns 200 OK (with the string as null).
Leaving me (for now) with one of the follwoing conclusions:
either MVC data validation does not work out of the box
either does not work as expected (if one marks a property as required it should be made sure it is there)
either MVC data validation is broken
either MVC data validation is completly useless
we are missing some important point (like the Bind[])
You get ModelValidation automatically as part of using/deriving from controller (I believe it is in the MVC middleware) but, unfortunately, this does not include null checks. So you need to explicitly check the parameter is NULL as well as the ModelState check.
[HttpPost]
public IActionResult Create([FromBody] RequestModel request)
{
if (request == null || !ModelState.IsValid)
{
return BadRequest(ModelState);
}
...
I assume you use
services.AddMvc();
so it should work by default.
But it doesn't work just as you expect: instead of returning 400 status code it invalidates model state and lets you manage action result.
You can create an attribute class to automatically return "Bad request"
internal class ValidateModelAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
if (!context.ModelState.IsValid)
{
context.Result = new BadRequestObjectResult(
new ApiError(
string.Join(" ",
context.ModelState.Values
.SelectMany(e => e.Errors)
.Select(e => e.ErrorMessage))));
}
}
}
where ApiError is a custom ViewModel for error results.
Now you can mark controllers or actions with this attribute to achieve a behavior you expect to have by default.
If you want this behavior for all methods just change your AddMvc line to something like this:
services.AddMvc(config => config.Filters.Add(new ValidateModelAttribute()));
After further experimenting around I found the answer.
Does the data validation need to be activated?
Answer: it depends on your configure services method:
No, it does not need to be activated if you use
services.AddMvc();
Yes, it needs to be activated if you use
services.AddMvcCore()
.AddDataAnnotations(); //this line activates it
This article brought me to the answer.
I am trying to execute a POST from my angular application to a .net Web API instance but the server returns null.
server
[HttpPost]
public string callBcknd([FromBody]string body)
{
try
{
Log.Info(string.Format("{0}", body));
}
catch(Exception ex)
{
return "error";
}
}
}
angular *note that I am using HttpClient instead of Http.. not sure if this is also the problem
callServer(){
var test = { "name": "John" }
let data = JSON.stringify(test);
let headers = new HttpHeaders();
headers.set('Content-Type', 'application/json');
this.appService.http.post('http://localhost:3000/api/WebApI/callBcknd',
test,
{headers: headers})
.subscribe(data => {console.log(data);}}}
config
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{action}/{id}",
defaults: new {action = "GET", id = RouteParameter.Optional}
);
}
}
With above setting, I don't gen any 404 server error in client side(by checking chrome's console) but it returns null in backend. But as I tried with Postman, it sends values properly with the same url. If I don't include [FromBody] inside of method in backend, I get an 404 server error in client side. Furthermore, messages says "NO HTTP resource was found that matches the request URI". Similar question to this seems solving the problem by having [FromBody] but I still get a null... I also suspected maybe my web config file(not the one in above) should contain some headers, so when I added some headers like content type to be json and etc then I get 500 server error in client side. At this point i am really confused and not sure what to proceed.
UPDATE1
Following server code returns the message but I am still getting the body as null.. no errors have been observed
[HttpPost]
public IHttpActionResult Callbcknd([FromBody] string body)
{
try
{
Log.Info(string.Format("called with data {0}", body));
return Ok(new { Message = "It worked!" });
}
catch(Exception ex)
{
return base.Content(HttpStatusCode.InternalServerError, ex.ToString());
}
}
I see multiple reasons why you would get unexpected errors and null values in your code:
(error) Your .net method callBcknd should not even compile as it can only return something if there is an Exception.
(error) You should send json when sending data to your api controller the message body and the api controller method should accept an complex object and not a primitive type like string/int/bool etc.
(warning) Your angular service should expose functionality and return either observables or promises that the component can then subscribe to. Do not expose the HttpClient directly.
(warning) Your web api should return interface IHttpActionResult instead of the type directly. Then you can use the built in methods like Ok and Content and BadRequest to return status information along with data. See also Action Results in Web API 2
(suggestion) Use Route and RoutePrefix as attributes instead of relying on the route config. This is more flexible and will allow you to also specify parameters to be included in the URL which will make for a more RESTful design. See also Attribute Routing in ASP.NET Web API 2
(suggestion) Add CamelCasePropertyNamesContractResolver to resolve between camel and pascal casing between your front end and backend. See also Serialization using ContractResolver
This is a good example of how make calls to a Web API and how to structure your code.
Note that these code samples only show the relevant parts that were added or modified
WebApiConfig.cs
public static class WebApiConfig {
public static void Register(HttpConfiguration config) {
// add this to ensure that casing is converted between camel case (front end) and pascal case (c#/backend)
var json = GlobalConfiguration.Configuration.Formatters.JsonFormatter;
json.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
config.MapHttpAttributeRoutes();
}
}
ApiModel.cs
public class ApiModel {
public string Content {get;set;}
}
WebApIController.cs
[RoutePrefix("api/WebApI")]
public class WebApIController : ApiController {
[HttpPost]
[Route("callBcknd")]
public IHttpActionResult CallBcknd([FromBody] ApiModel body)
{
try
{
Log.Info(string.Format("{0}", body.Content));
return Ok(new {Message = "It worked!"});
}
catch(Exception ex)
{
// example of how to return error with content. I would not recommend actually returning the exception details to the client in a production setting
return base.Content(HttpStatusCode.InternalServerError, ex.ToString());
}
}
}
application.service.ts
constructor(private httpClient: HttpClient){}
callServer(data: {content: string}) : Observable<any> {
return this.httpClient.post('http://localhost:3000/api/WebApI/callBcknd', data);
}
application.component.ts
constructor(private myService: MyService){}
onDoSomething(){
this.myService.callServer({content: 'This is what I have sent'})
.subscribe(data => console.log("Succeeded, result = " + data), (err)=> console.error("Failed! " + err));
}
Notice the following:
ApiModel represents the incoming object in the request. The angular call then sends {content: 'This is what I have sent'} which mirrors this type.
IHttpActionResult is the response type for your Web API method
You can return different types along with status information in the method CallBcknd
Route and RoutePrefix were added to give more control over the uri path.
The angular component and service have been split into 2 methods, the service returns an observable and the component calls the service methods and subcribes to the returning observable. When you extend this example you want to replace any with defined expected results using interfaces and the same is true for any incoming parameters you want to send.
A Tipical call to a API from "Angular"
update(data: string): Observable<IResponse> {
console.log(data);
let url = '...';
let headers = new Headers({
'Content-Type': 'application/json; charset=utf-8',
});
let options = new RequestOptions({ headers: headers })
return this._http.post(url, data, options)
.map((res: any) => {
return res.json();
})
.catch(this.handleError);
}
The code in API
[HttpPost]
public string callBcknd([FromBody]string body)
{
try
{
Log.Info(string.Format("{0}", body));
//You must return something
return "Post Realized";
}
catch(Exception ex)
{
return "error";
}
}
//I like call async
[HttpPost]
public async Task<IActionResult>callBcknd([FromBody]string body)
{
try
{
Log.Info(string.Format("{0}", body));
//await "some action"
//You can return OK("success") or an object
return Ok(new { success = true, description = "callback sucesfully" });;
}
catch(Exception ex)
{
//You can return OK("error") or an object
return Ok(new { success = false, description = ex.InnerException });;
}
}
Well, what you're posting would look something like
{"body":{// something here }}
Whereas your controller expects:
"valuehere" (which is a valid json for string).
You need to change the c# code to have a model for your DTO:
public class PostedObject{
public object Data {get;set;}
}
[HttpPost]
public string callBcknd([FromBody]PostedObject body)
{
// do things
}
I am trying to make a PUT call to my service to update a user
I get 404 when code hits my URL but in dev tools if I click URL it fails at it actually hits my controller. I'm thinking I'm passing my model wrong as parameter, can someone please point to me proper direction please, I'm not sure what I'm doing wrong.
here is my service call
var updateUser = function(user) {
return $http({
method: "PUT",
url: serviceBase + "/UserController/PutUser",
data: user
}).success(successCallBack).error(errorCallBack);
}
and here is my controller
[HttpPut]
[Route("api/UserController/PutUser")]
public IHttpActionResult PutUser(UserDto user)
{
try
{
return Ok();
}
catch (Exception)
{
return NotFound();
}
}
I tried using JSON.stringify(user) in my $http call as well and was out of luck get same 404 error but after clicking actual link it will hit my controller
Thank you for your advise!
While the common practice is to also include an identifier with PUT requests you can still achieve what you desire with some minor changes.
First update the URL for the intended endpoint in the controller.
NOTE: This is just an example. Modify to suit your specific needs.
public class UsersController : ApiController {
[HttpPut]
[Route("api/Users")]
public IHttpActionResult Put(User user) {
try {
//assuming some form of storage for models
repository.Users.Update(user);
return Ok();
} catch (Exception) {
return NotFound();
}
}
}
... and then update your service call...
var updateUser = function(user) {
return $http({
method: "PUT",
url: serviceBase + "/api/Users",
data: user
}).success(succesCallBAck).error(errorCallBack);
}
the user will be sent in the body of the request to the controller.
You need to put the id of the user at the end of url. Your methods needs 2 parameters. One userId and one User.
You may also want to revise your url structure.