I've implemented a custom error controller to show the user custom error views. I followed a tutorial (can't find it anymore), so my controller looks like this:
#Controller
public class CustomErrorController implements ErrorController {
#RequestMapping("/error")
public String handleError(HttpServletRequest request) {
Object status = request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
if (status != null) {
Integer statusCode = Integer.valueOf(status.toString());
if (statusCode == HttpStatus.FORBIDDEN.value()) {
return "error/403";
} else if (statusCode == HttpStatus.NOT_FOUND.value()) {
return "error/404";
} else if (statusCode == HttpStatus.INTERNAL_SERVER_ERROR.value()) {
return "error/500";
}
}
return "error/default";
}
#Override
public String getErrorPath() {
return "/error";
}
}
So far so good, but since May 17, 2019 SonarQube complains about a #RequestMapping without a method. So I added the 4 methods I am using:
#RequestMapping(value = "/error", method = { RequestMethod.GET, RequestMethod.POST, RequestMethod.PUT,
RequestMethod.DELETE })
But Sonar now complains that I have too many methods. So what is the correct way to implement a custom ErrorController that complies with this Sonar rule?
Since that is your "default error page" route, it will always be GET due to redirection, so you can safely change to #GetMapping.
Follow this and write in web.xml
<display-name>App Name </display-name>
<error-page>
<error-code>500</error-code>
<location>/error500.jsp</location>
</error-page>
<error-page>
<error-code>404</error-code>
<location>/error404.jsp</location>
</error-page>
Spring MVC: How to return custom 404 errorpages?
You can suppress the specific Sonar rule using #SuppressWarnings("squid:S3752"), like this:
#Controller
public class CustomErrorController implements ErrorController {
#SuppressWarnings("squid:S3752")
#RequestMapping("/error")
public String handleError(HttpServletRequest request) {
...
}
It is better to disable just this specific rule using #SuppressWarnings, rather than, as suggested in a comment, disabling Sonar completely for the line using //NOSONAR (see this question).
The answer by Antoniossss (changing to #GetMapping) is unfortunately not correct.
Spring will not send a redirect back to the browser but will redirect to your CustomErrorController internally - and the redirected request will use the same method as the original request that caused the error.
If you limit the handleError() method to only handle the GET method the client will get a 405 Method not allowed response if the original request was a POST.
Related
We are using spring and spring-security-3.2. Recently We are adding annotations #PreAuthorize to RestAPIs(earlier it was URL based).
#PreAuthorize("hasPermission('salesorder','ViewSalesOrder')")
#RequestMapping(value = "/restapi/salesorders/", method = RequestMethod.GET)
public ModelAndView getSalesOrders(){}
We already have Global exception handler which annotated with - #ControllerAdvice and custom PermissionEvaluator in place, everything works fine except the error message.
Lets say some user is accessing API At moment without having 'ViewSalesOrder' permission then spring by default throws the exception 'Access is denied',but didn't tell which permission is missing (Its our requirement to mention which permission is missing).
Is it possible to throw an exception which also include the permission name, so final error message should be look like "Access is denied, you need ViewSalesOrder permission"(here permission name should be from #PreAuthorize annotation)?
Please note that we have 100 such restAPI in place so generic solution will be highly appreciated.
There is no pretty way of achieving what you expect since PermissionEvaluator interface doesn't let you pass the missing permission along with the result of the evaluation.
In addition, AccessDecisionManager decides on the final authorization with respect to the votes of the AccessDecisionVoter instances, one of which is PreInvocationAuthorizationAdviceVoter which votes with respect to the evaluation of #PreAuthorize value.
Long story short, PreInvocationAuthorizationAdviceVoter votes against the request (giving the request –1 point) when your custom PermissionEvaluator returns false to hasPermission call. As you see there is no way to propagate the cause of the failure in this flow.
On the other hand, you may try some workarounds to achieve what you want. One way can be to throw an exception within your custom PermissionEvaluator when permission check fails. You can use this exception to propagate the missing permission to your global exception handler. There, you can pass the missing permission to your message descriptors as a parameter. Beware that this will halt execution process of AccessDecisionManager which means successive voters will not be executed (defaults are RoleVoter and AuthenticatedVoter). You should be careful if you choose to go down this path.
Another safer but clumsier way can be to implement a custom AccessDeniedHandler and customize the error message before responding with 403. AccessDeniedHandler provides you current HttpServletRequest which can be used to retrieve the request URI. However, bad news in this case is, you need a URI to permission mapping in order to locate the missing permission.
I have implemented the second possible solution mentioned by Mert Z. My solution works only for #PreAuthorize annotations used in the API layer (e.g. with #RequestMapping). I have registered a custom AccessDeniedHandler bean in which I get the value of the #PreAuthorize annotation of the forbidden API method and fills it into error message.
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
private DispatcherServlet dispatcherServlet;
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException,
ServletException {
if (!response.isCommitted()) {
List<HandlerMapping> handlerMappings = dispatcherServlet.getHandlerMappings();
if (handlerMappings != null) {
HandlerExecutionChain handler = null;
for (HandlerMapping handlerMapping : handlerMappings) {
try {
handler = handlerMapping.getHandler(request);
} catch (Exception e) {}
if (handler != null)
break;
}
if (handler != null && handler.getHandler() instanceof HandlerMethod) {
HandlerMethod method = (HandlerMethod) handler.getHandler();
PreAuthorize methodAnnotation = method.getMethodAnnotation(PreAuthorize.class);
if (methodAnnotation != null) {
response.sendError(HttpStatus.FORBIDDEN.value(),
"Authorization condition not met: " + methodAnnotation.value());
return;
}
}
}
response.sendError(HttpStatus.FORBIDDEN.value(),
HttpStatus.FORBIDDEN.getReasonPhrase());
}
}
#Inject
public void setDispatcherServlet(DispatcherServlet dispatcherServlet) {
this.dispatcherServlet = dispatcherServlet;
}
}
The handler is registered in WebSecurityConfigurerAdapter:
#EnableGlobalMethodSecurity(jsr250Enabled = true, prePostEnabled = true)
#EnableWebSecurity
public abstract class BaseSecurityInitializer extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
...
http.exceptionHandling().accessDeniedHandler(accessDeniedHandler());
...
}
#Bean
public AccessDeniedHandler accessDeniedHandler() {
return new CustomAccessDeniedHandler();
}
}
Beware that if there is also a global resource exception handler with #ControllerAdvice the CustomAccessDeniedHandler won't be executed. I solved this by rethrowing the exception in the global handler (as advised here https://github.com/spring-projects/spring-security/issues/6908):
#ControllerAdvice
public class ResourceExceptionHandler {
#ExceptionHandler(AccessDeniedException.class)
public ResponseEntity accessDeniedException(AccessDeniedException e) throws AccessDeniedException {
log.info(e.toString());
throw e;
}
}
You can throw an org.springframework.security.access.AccessDeniedException from a method that was called inside an EL-Expression:
#PreAuthorize("#myBean.myMethod(#myRequestParameter)")
Ideally, the #PreAuthorize annotation should be supporting String message(); in addition to the SpEl value. But, for whatever reason, it does not. Most of the suggestions here seem unnecessarily cumbersome and elaborate. As #lathspell has suggested, the simplest way to provide your own error message - along with any custom access validation logic - would be to add a simple method that performs the check and throws the AccessDeniedException in case the check fails, and then reference that method in the SpEl expression. Here's an example:
#RestController
#RequiredArgsConstructor // if you use lombok
public class OrderController {
private final OrderService orderService;
...
#GetMapping(value = "/salesorders", produces = MediaType.APPLICATION_JSON_VALUE)
#PreAuthorize("#orderController.hasPermissionToSeeOrders(#someArgOfThisMethod)")
public Page<OrderDto> getSalesOrders(
// someArgOfThisMethod here, perhaps HttpRequest, #PathVariable, #RequestParam, etc.
int pageIndex, int pageSize, String sortBy, String sortOrder) {
Pageable pageRequest = PageRequest.of(pageIndex, pageSize, Sort.Direction.fromString(sortOrder), sortBy);
return ordersService.retrieveSalesOrders(..., pageRequest);
}
public static Boolean hasPermissionToSeeOrders(SomeArgOfTheTargetMethod argToEvaluate) {
//check eligibility to perform the operation based on some data from the incoming objects (argToEvaluate)
if (condition fails) {
throw new AccessDeniedException("Your message");
}
return true;
}
I've been struggling with a rather absurd problem for a few days now:
The project I'm on is using Spring MVC with FreeMarker for it's templating.
This is running atop a Tomcat container (testing locally using Cargo).
The issue I'm working has the brief of implementing uniform behaviour in a standardised error page but covering covering the various types of errors that may be encountered. (Exceptions bubbling up from back-end services, inadequate permissions, http errors, etc)
So far, the results are as follows (Graphic included):
Fig A: Normal navigation to page - renders as expected.
Fig B & Fig C: Service and Permission Exceptions caught by ControllerAdvice.java - likewise, no issues.
Fig D: Any HTTP Error (yes, even 418 if you trigger that response) - Inner freemarker template is correctly retrieved and populated with bindings but decorations applied by filters fail to trigger.
Currently we're using Spring to configure the servlet handling so the web.xml is beautifully sparse:
web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">
<!--
This application uses the config of the mapping by Spring MVC
This is why you will not see servlet declarations here
The web app is defined in
- butler.SpringWebInit
- butler.SpringWebConfig
-->
<context-param>
<description>Escape HTML form data by default when using Spring tags</description>
<param-name>defaultHtmlEscape</param-name>
<param-value>true</param-value>
</context-param>
<!-- Disabling welcome list file for Tomcat, handling it in Spring MVC -->
<welcome-file-list>
<welcome-file/>
</welcome-file-list>
<!-- Generic Error redirection, allows for handling in Spring MVC -->
<error-page>
<location>/http-error</location>
<!-- Was originally just "/error" it was changed for internal forwarding/proxying/redirection attempts -->
</error-page>
</web-app>
The Configuration is handled by SpringWebInit.java to which I have not made any modifications:
SpringWebInit.java
/**
* Automatically loaded by class org.springframework.web.SpringServletContainerInitializer
*
* #see http://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle/#mvc-container-config
*
* According to {#link AbstractSecurityWebApplicationInitializer}, this class should be
* annotated with a Order so that it is loaded before {#link SpringSecurityInit}
*/
#Order(0)
public class SpringWebInit extends AbstractAnnotationConfigDispatcherServletInitializer implements InitializingBean {
private final Logger LOG = LoggerFactory.getLogger(getClass());
#Override
public void afterPropertiesSet() throws Exception {
LOG.info("DispatcherServlet loaded");
}
#Override
protected Class<?>[] getServletConfigClasses() {
return null; // returning null, getRootConfigClasses() will handle this as well
}
#Override
protected String[] getServletMappings() {
return new String[] {"/**"}; // Spring MVC should handle everything
}
#Override
protected Class<?>[] getRootConfigClasses() {
return new Class[] {SpringWebConfig.class, SpringSecurityConfig.class};
}
#Override
protected Filter[] getServletFilters() {
CharacterEncodingFilter characterEncodingFilter =
new CharacterEncodingFilter(StandardCharsets.UTF_8.name(), true);
return new Filter[] {characterEncodingFilter, new SiteMeshFilter()};
}
}
Which in turn loads The various config for Freemarker and Sitemesh:
SpringWebConfig.java
#EnableWebMvc
#Configuration
#PropertySource("classpath:/butler-init.properties")
#ComponentScan({"butler"})
class SpringWebConfig extends WebMvcConfigurerAdapter implements InitializingBean {
private final Logger LOG = LoggerFactory.getLogger(getClass());
#Autowired
LoggedInUserService loggedInUserService;
#Override
public void afterPropertiesSet() throws Exception {
LOG.info("Web Mvc Configurer loaded");
}
#Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(userHeaderInterceptor());
}
#Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/static/**").addResourceLocations("/static/").setCacheControl(
CacheControl.maxAge(30, TimeUnit.MINUTES).noTransform().cachePublic().mustRevalidate());
}
#Bean
FreeMarkerViewResolver viewResolver() throws TemplateException {
FreeMarkerViewResolver resolver = new FreeMarkerViewResolver();
resolver.setCache(/*true*/false); // Set to false for debugging
resolver.setPrefix("");
resolver.setSuffix(".ftlh");
resolver.setRequestContextAttribute("rContext");
resolver.setContentType("text/html;charset=UTF-8");
DefaultObjectWrapper wrapper =
new DefaultObjectWrapperBuilder(freemarker.template.Configuration.getVersion()).build();
Map<String, Object> attrs = new HashMap<>();
attrs.put("loggedInUserService", wrapper.wrap(loggedInUserService));
resolver.setAttributesMap(attrs);
return resolver;
}
#Bean
FreeMarkerConfigurer freeMarkerConfig() {
Properties freeMarkerVariables = new Properties();
// http://freemarker.org/docs/pgui_config_incompatible_improvements.html
// http://freemarker.org/docs/pgui_config_outputformatsautoesc.html
freeMarkerVariables.put(freemarker.template.Configuration.INCOMPATIBLE_IMPROVEMENTS_KEY,
freemarker.template.Configuration.getVersion().toString());
FreeMarkerConfigurer freeMarkerConfigurer = new FreeMarkerConfigurer();
freeMarkerConfigurer.setDefaultEncoding("UTF-8");
freeMarkerConfigurer.setTemplateLoaderPath("/WEB-INF/mvc/view/ftl/");
freeMarkerConfigurer.setFreemarkerSettings(freeMarkerVariables);
return freeMarkerConfigurer;
}
#Bean
UserHeaderInterceptor userHeaderInterceptor() {
return new UserHeaderInterceptor();
}
#Bean
static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
return new PropertySourcesPlaceholderConfigurer();
}
}
SiteMeshFilter.java
public class SiteMeshFilter extends ConfigurableSiteMeshFilter {
#Override
protected void applyCustomConfiguration(SiteMeshFilterBuilder builder) {
// Don't use decorator REST api pages
builder.addExcludedPath("/api/*");
builder.addDecoratorPath("/*", Views.DECORATOR_HEADER_FOOTER);
builder.setIncludeErrorPages(true);
}
}
Finally, onto the meat of the problem, the error handling is being handled via a combination of DefaultControllerAdvice.java, which provides the rules for intercepting exceptions and ErrorController.java itself, which handles the mappings and eventually, the message handling (displaying information about the error, adapting according to the type of error, etc)
DefaultControllerAdvice.java
#ControllerAdvice(annotations = Controller.class)
class DefaultControllerAdvice {
private static String EXCEPTION = "butlerexception";
#ExceptionHandler(ServiceException.class)
public String exceptionHandler(ServiceException se, Model model) {
model.addAttribute(EXCEPTION, se.getMessage());
return Views.ERROR;
}
#ExceptionHandler(PermissionException.class)
public String exceptionHandler(PermissionException pe, Model model) {
model.addAttribute(EXCEPTION, "Incorrect Permissions");
return Views.ERROR;
}
/*#ResponseStatus(HttpStatus.NOT_FOUND)
#ExceptionHandler(IOException.class)
public String exceptionHandler(Model model) { // Trying another way of intercepting 404 errors
model.addAttribute(EXCEPTION, "HTTP Error: 404");
return Views.ERROR;
}*/
}
ErrorController.java
#Controller
class ErrorController extends AbstractController {
#Autowired
private LoggedInUserService loggedInUserService;
#RequestMapping(path="error",method = {GET,POST}) // Normal Error Controller, Returns fully decorated page without issue for Exceptions and normal requests.
public String error(RedirectAttributes redirectAttributes, HttpServletResponse response,Model model) {
//if (redirectAttributes.containsAttribute("errorCode")) { // Trying to invisibly use redirection
// Map<String, ?> redirAttribs = redirectAttributes.getFlashAttributes();
// model.addAttribute("butlerexception", "HTTP Error: "+redirAttribs.get("errorCode"));
//} else {
model.addAttribute("butlerexception", "Error");
//}
return ERROR;
}
#RequestMapping("/http-error") // Created to test HTTP requests being proxied via ServiceExceptions, Redirections, etc...
public String httpError(/*RedirectAttributes redirectAttributes,*/ HttpServletResponse response, HttpServletRequest request, Model model){
model.addAttribute("butlerexception", "HTTP Error: " + response.getStatus());
//throw new ServiceException("HTTP Error: " + response.getStatus()); // Trying to piggyback off Exception handling
//redirectAttributes.addFlashAttribute("errorCode", response.getStatus()); // Trying to invisibly use redirection
//redirectAttributes.addFlashAttribute("originalURL",request.getRequestURL());
return /*"redirect:"+*/ERROR;
}
}
So Far, I have tried:
Throwing exceptions to piggy-back off the working ControllerAdvice rules. - Result was undecorated.
Adding in Rules for response codes, IONotFound nad NoHandlerFound exceptions - Result was undecorated.
Redirecting to the error page - Result was decorated correctly, but URL and response codes were incorrect, attempting to mask the URL with the original request URL resulted in the correct URL and code, but the same lack of decoration as before.
Additionally, from the debugging logs, I can see that the filters from Spring Security are triggered normally but the ones involved with decorating the site (for both logged in and anonymous requests) fail to trigger for HTTP errors only.
One of the limiting factors currently is that I cannot gut the system and define it all in the web.xml (as many of the solutions here and in the Spring documentation seem to call for) without causing excessive disruption to development at this stage. (nor do I have the authority to effect such a change (Junior rank))
For Convenience sake, a few of the solutions I've tried so far:
Spring MVC 404 Error Page
404 error redirect in Spring with java config
Generic Error Page not decorated
Custom Error Page Not Decorated by Sitemesh in Spring Security Application
Custom 404 using Spring DispatcherServlet
<error-page> setup doesn't work in Spring MVC
At this point I'm really not sure what else to try, what on earth am I missing here?
Edit: it turned out to be a bug in SiteMesh to do with the triggering of .setContentType(...) that was solved via setting the contentType again after sitemesh in order to trigger decoration: Bug report with description and solution
This turned out to a two-part issue, firstly SiteMesh3's handling of error pages means that it believes it has processed all the filters even when an error causes decorators to be skipped. (expanded upon in this issue on github)
The second part was that SiteMesh3 appears to only buffer pages for decoration when SpringMVC calls .setContentType(...).
This was tripping up since Spring will only trigger this on elements with undefined content type whereas errors have already had their content type defined before they even reach Spring. (expanded upon by my lead in this issue)
My lead managed to solve this by adding a filter after SiteMesh that triggered .setContentType(...) and forced SiteMesh to buffer the page for decoration.
It's a little heavy, since it means that the content type is set twice per request, but it works.
Edit: Originally had a note here asking not to upvote to avoid receiving rep for a solution my lead found, but found a blog post explaining that self-answers don't earn rep - huzzah!
Solution 1:
Check if you have disabled property spring.resources.add-mappings=false. Enabling it could solve the problem. But in my case enabling it removed custom error pages at all.
Solution 2:
Based on comments on github issue https://github.com/sitemesh/sitemesh3/issues/25 declare custom selector inside your SiteMeshFilter:
public class SiteMeshFilter extends ConfigurableSiteMeshFilter {
#Override
protected void applyCustomConfiguration(SiteMeshFilterBuilder builder) {
builder.setCustomSelector(new CustomBasicSelector());
}
private static class CustomBasicSelector extends BasicSelector {
private static final String ALREADY_APPLIED_KEY = BasicSelector.class.getName() + ".APPLIED_ONCE";
public CustomBasicSelector() {
super(true, "text/html");
}
protected boolean filterAlreadyAppliedForRequest(HttpServletRequest request) {
if (request.getDispatcherType().equals(DispatcherType.ERROR)) {
if (Boolean.TRUE.equals(request.getAttribute(ALREADY_APPLIED_KEY + ".ERROR"))) {
return true;
} else {
request.setAttribute(ALREADY_APPLIED_KEY + ".ERROR", true);
return false;
}
}
return super.filterAlreadyAppliedForRequest(request);
}
}
}
I have a simple rest controller :
#RestController
#RequestMapping("/api/v1/")
public class OrderController {
#RequestMapping(value = "/orders2", method = RequestMethod.POST)
public OrderDto createOrder2(#RequestBody OrderDto order) throws Exception {
throw new Exception("Bouh!");
}
}
And I want to manage exceptions globally. From what I read it can be done with something like :
#ControllerAdvice
public class ErrorController {
#ExceptionHandler(Exception.class)
#ResponseStatus(value = HttpStatus.BAD_REQUEST)
public ErrorDto handleConflict(HttpServletRequest request, Exception e) throws Exception {
ErrorDto o = new ErrorDto ();
o.setMessage(e.getMessage());
return o;
}
}
But when I make a post on my request, I get the following error :
26/10/2016 17:26:08.187 [http-nio-8080-exec-12] WARN o.s.web.servlet.PageNotFound -
No mapping found for HTTP request with URI [/duorest/api/v1/api/v1/orders2]
in DispatcherServlet with name 'rest'
I don't know why the uri change to /duorest/api/v1/api/v1/orders2
Some facts :
I checked in debug, my code is executed
If I move the method in the rest controller, I get no error and what I expect (my ErrorDto object)
Spring framework version 4.3.3.RELEASE
Spring-data-rest-webmvc version 2.5.4.RELEASE
Anybody already had this problem ? Or any hint ?
Is it resolved? if not please try to execute with #ResponseBody is missing on the handleConflict method.
In my spring MVC app (3.1.4-release, Servlet 2.5),
I am not able to display a custom 403 error page when trigger from my HandlerInterceptorAdapter.
My web.xml :
<error-page>
<error-code>404</error-code>
<location>/404</location>
</error-page>
<error-page>
<error-code>403</error-code>
<location>/403</location>
</error-page>
My Error Controller :
#Controller
public class HTTPErrorController {
#RequestMapping(value="403")
public ModelAndView error403(){
return new ModelAndView("/errors/403");
}
#RequestMapping(value="404")
public ModelAndView error404(){
return new ModelAndView("/errors/404");
}
}
My Handler interceptor :
public class CSRFHandlerInterceptor extends HandlerInterceptorAdapter {
#Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// Validate CSRF token on POST request only.
if (request.getMethod().equalsIgnoreCase("post")) {
if (!CSRFTokenManager.verifyCSRFToken(request)) {
response.sendError(HttpServletResponse.SC_FORBIDDEN, "Bad or missing CSRF value");
return false;
}
}
return true;
}
When the sendError is sent, I have a blank page in firefox and in Ie.
404 exception is working correctly but can't figure out how to make the 403 display my custom error page.
Firefox screenshot:
http://hpics.li/8b6ab9f
Httpfox screenshot :
http://hpics.li/b032ca0
** I am using apache-tomcat-7.0.42
Thanks
I got a similar problem.
For my situation, I figured out it is because my error page do response.forward and than goto Interceptor again. (infinite loop)
i.e., You can try to simplify your /error/403 page to plaintext: "Here is 403".
I want to return a HTTP status 404 if invalid arguments are passed to my controller. For example if I have a controller that looks like:
public ActionResult GetAccount(int id)
{
...
}
Then I want to return a 404 if say urls such as these are encountered:
/GetAccount
/GetAccount/notanumber
i.e. I want to trap the ArgumentException that is thrown.
I know I could use a nullable type:
public ActionResult GetAccount(int? id)
{
if(id == null) throw new HttpException(404, "Not found");
}
But that's pretty icky and repetitious.
I was hoping I could add this to my controllers where necessary:
[HandleError(View="Error404", ExceptionType = typeof(ArgumentException))]
public class AccountsController : Controller
{
public ActionResult GetAccount(int id)
{
...
}
}
But that doesn't appear to work well.
I saw this post and this answer which nearly solves my problem:
In that answer an abstract BaseController is created from which you derive all your other controllers from:
public abstract class MyController : Controller
{
#region Http404 handling
protected override void HandleUnknownAction(string actionName)
{
// If controller is ErrorController dont 'nest' exceptions
if (this.GetType() != typeof(ErrorController))
this.InvokeHttp404(HttpContext);
}
public ActionResult InvokeHttp404(HttpContextBase httpContext)
{
IController errorController = ObjectFactory.GetInstance<ErrorController>();
var errorRoute = new RouteData();
errorRoute.Values.Add("controller", "Error");
errorRoute.Values.Add("action", "Http404");
errorRoute.Values.Add("url", httpContext.Request.Url.OriginalString);
errorController.Execute(new RequestContext(
httpContext, errorRoute));
return new EmptyResult();
}
#endregion
}
This works great at handling unknown actions with a 404 but doesn't allow me to handle invalid data as a 404.
Can I safely override Controller.OnException(ExceptionContext filterContext) like this:
protected override void OnException(ExceptionContext filterContext)
{
if(filterContext.Exception.GetType() == typeof(ArgumentException))
{
filterContext.ExceptionHandled = true;
this.InvokeHttp404(filterContext.HttpContext);
}
else
{
base.OnException(filterContext);
}
}
On the surface it seems to work, but am I storing up any problems by doing this?
Is this semantically correct thing to do?
Best way? Action method selector attribute!
To actually avoid nullable method arguments I suggest that you write an Action Method Selector attribute that will actually only match your action method when id is supplied. It won't say that argument wasn't supplied but that it couldn't match any action methods for the given request.
I would call this action selector RequireRouteValuesAttribute and would work this way:
[RequireRouteValues("id")]
public ActionResult GetAccount(int id)
{
...
}
Why is this the best solution for your problem?
If you look at your code you'd like to return a 404 on actions that match name but parameter binding failed (either because it wasn't supplied or any other reason). Your action so to speak requires particular action parameter otherwise a 404 is returned.
So when adding action selector attribute adds the requirement on the action so it has to match name (this is given by MVC) and also require particular action parameters. Whenever id is not supplied this action is not matched. If there's another action that does match is not the issue here because that particular action will get executed. The main thing is accomplished. Action doesn't match for invalid route request and a 404 is returned instead.
There's an app code for that!
Check my blog post that implements this kind of attribute that you can use out of the box. It does exactly what you're after: it won't match your action method if route data provided doesn't have all required values.
Disclaimer: this does not cover all the cases
For urls in your examples, returning 404 can be done in single line. Just add route constraint for id parameter.
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index" }, // Parameter defaults
new { id = #"\d+" } // restrict id to be required and numeric
);
And that's all. Now any matching url that has no id or id is not numeric, autimatically triggers not found error (for which there are plenty of ways to handle, one in your example, another by using custom HandleErrorAttribute, etc). And you can use non-nullable int parameters on your actions.
I managed to get this working by adding this route at the end of all routes:
routes.MapRoute("CatchAllErrors", "{*url}",
new { controller = "Error", action = "NotFound" }
);
Note: First I followed this: How can I properly handle 404 in ASP.NET MVC?