1

Trying to provide a custom 404 error page in a web application that, to the best of my knowledge, uses Java Config (thus no web.xml).

We have the following versions of the related libraries: spring ("5.1.2.RELEASE"), spring-security ("5.1.1.RELEASE").

Disclaimer

I have checked different approaches here in StackOverflow. Please don't suggest results for web.xml, Thymeleaf or Spring Boot. This is not applicable.

Among others; I tried with the following approaches:

  • @Controller annotation (here and here)
  • adding a web.xml

None produced the expected result (that is, still getting the default webserver layout and error).

Controller annotation approach

  • exception package

    package ...;
    
    import org.springframework.http.HttpStatus;
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.ControllerAdvice;
    import org.springframework.web.bind.annotation.ExceptionHandler;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    import org.springframework.web.bind.annotation.ResponseBody;
    import org.springframework.web.servlet.NoHandlerFoundException;
    
    @ControllerAdvice
    public class GlobalExceptionHandler {
    
        // Option A (used as an alternative to option B)
        //@ExceptionHandler(Exception.class)
        //public String handle(Exception ex) {
        //   return "redirect:/404";
        //}
    
        @RequestMapping(value = {"/404"}, method = RequestMethod.GET)
        public String NotFoundPage() {
            return "404";
        }
    
        // Option B (used as an alternative to option A)
        @ExceptionHandler(Exception.class)
        public ResponseEntity<String> handleNoHandlerFoundException(GlobalExceptionHandler ex) {
            ResponseEntity responseEntity = new ResponseEntity<>(new RestClientException("Testing exception"),
                HttpStatus.NOT_FOUND);
            return responseEntity;
        }
    }
    
  • init class

    package ...;
    
    import org.springframework.context.annotation.ComponentScan;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.PropertySource;
    import org.springframework.transaction.annotation.EnableTransactionManagement;
    import org.springframework.web.servlet.config.annotation.EnableWebMvc;
    import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
    
    import org.springframework.web.bind.annotation.ExceptionHandler;
    import org.springframework.http.ResponseEntity;
    import org.springframework.http.HttpStatus;
    import org.springframework.web.servlet.NoHandlerFoundException;
    import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
    
    @Configuration
    @ComponentScan("...")
    @EnableWebMvc
    @EnableTransactionManagement
    @PropertySource("classpath:application.properties")
    public class WebAppConfig extends WebMvcConfigurerAdapter {
    
        @ExceptionHandler({ Exception.class })
        public ResponseEntity<RestClientException> handle(NoHandlerFoundException e) {
            return new ResponseEntity<>(new RestClientException("Testing exception"), HttpStatus.NOT_FOUND);
        }
    
        ...
    
        @Override
        public void addViewControllers(ViewControllerRegistry registry) {
            super.addViewControllers(registry);
            registry.addViewController("/404.jsp").setViewName("404");
        }
    }
    

There is also an Initializer class (public class Initializer implements WebApplicationInitializer), which seems to conflict with some suggested options (defined here and here); so the webapp-init class is not modified.

web.xml approach

<?xml version="1.0" encoding="UTF-8"?>
<web-app id="ROOT" xmlns="http://java.sun.com/xml/ns/j2ee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"
    version="2.4">

<error-page>
  <error-code>404</error-code>
  <location>/error</location>
</error-page>
<error-page>
    <exception-type>java.lang.Exception</exception-type>
    <location>/error</location>
</error-page>

</web-app>

The 404.jsp or 404.html files are placed (currently for testing purposes at all the following locations):

    src/main/resources
    ├── ...
    ├── error
    │   └── 404.html
    ├── public
    │   ├── 404.html
    │   └── error
    │       └── 404.html
    ├── templates
    │   └── 404.html
    └── ...

    src/main/webapp/WEB-INF/
    ├── error.jsp
    ├── tags
    │   └── ...
    └── views
        ├── 404.html
        ├── 404.jsp
        ├── error.jsp
        └── ...

Any idea on what is missing or wrong?

Kiddo
  • 158
  • 1
  • 4
  • 12
  • probably already has an answer on stackOverflow, like https://stackoverflow.com/questions/37398385/spring-boot-and-custom-404-error-page – Dmitri Algazin Mar 06 '19 at 17:10
  • I don't think so. You point to something with Spring-Boot or Thymeleaf. That is not my case. I added some references of suggestions like these which did not work for me. – Kiddo Mar 06 '19 at 17:12
  • Try to follow same folder structure. As said in there "If you want to display a custom HTML error page for a given status code, you add a file to an /error folder." I don't try to be cleaver, just do something that works and later customize for your needs. simple – Dmitri Algazin Mar 06 '19 at 17:17
  • I have just tried with the same structure (under `src/main/resources/public/error/`); yet no luck – Kiddo Mar 06 '19 at 17:27
  • Find where they mention ErrorController, as I understood you can extend that with custom error path, or do logic as they did. I would put breakpoints in BasicErrorController and AbstractErrorController everywhere and see what the flow is. – Dmitri Algazin Mar 06 '19 at 17:37
  • Hmm, sorry... I fail to see how that can help. First, that is for Spring Boot so ErrorController cannot be imported. Second, even if it was, for normal Spring that's a normal `@RequestMapping` directive which is not used as fallback for error handling but only for explicit redirection (`return /error` and the like). What I want to do is to provide a default handler for 404 (and these cannot be redirected from the normal Controller classes, afaik) – Kiddo Mar 07 '19 at 10:26

3 Answers3

1

Although not as clear as I would like, this is a sort of working version to at least provide some customisation to error pages. It is a first approach but hopefully can help others.

The list of handled exceptions is not extensive, but mainly addressing 404 errors (NoHandlerFoundException) and other typical errors like InternalServerErrorException and NullPointerException, to try to catch them all in the end with a generic error for everything else that is an Exception).

Note that this is not covering other exceptions related to e.g. bad syntax in a JSTL template (org.apache.jasper.*; exceptions that cannot apparently be caught here).

These are the related changes and additions to the source base:

CustomSimpleMappingExceptionResolver.java (provide generic exceptions, yet logs details)

package ...;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import javax.ws.rs.InternalServerErrorException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.http.HttpStatus;
import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.NoHandlerFoundException;

public class CustomSimpleMappingExceptionResolver extends SimpleMappingExceptionResolver {

    public CustomSimpleMappingExceptionResolver() {
        // Turn logging on by default
        setWarnLogCategory(getClass().getName());
    }

    @Override
    public String buildLogMessage(Exception e, HttpServletRequest req) {
        return "MVC exception: " + e.getLocalizedMessage();
    }

    @Override
    protected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response,
                                              Object handler, Exception ex) {

        // Log exception
        ex.printStackTrace();
        String exceptionCause = ex.toString();
        String exceptionType = ex.getClass().getCanonicalName();

        // Get the ModelAndView to use
        ModelAndView mav = super.doResolveException(request, response, handler, ex);

        // Make more information available to the view - note that SimpleMappingExceptionResolver adds the exception already
        mav.addObject("url", request.getRequestURL());
        mav.addObject("timestamp", new Date());

        ArrayList<String> exceptions404 = new ArrayList<String>(
                Arrays.asList(
                        NoHandlerFoundException.class.getName()
                        )
        );
        ArrayList<String> exceptions500 = new ArrayList<String>(
                Arrays.asList(
                        InternalServerErrorException.class.getName(),
                        NullPointerException.class.getName()
                        )
        );

        String userExceptionDetail = ex.toString();
        String errorHuman = "";
        String errorTech = "";

        if (exceptions404.contains(exceptionType)) {
            errorHuman = "We cannot find the page you are looking for";
            errorTech = "Page not found";
            userExceptionDetail = String.format("The page %s cannot be found", request.getRequestURL());
            mav.setViewName("/error/404");
            mav.addObject("status", HttpStatus.NOT_FOUND.value());
        } else if (exceptions500.contains(exceptionType)) {
            errorHuman = "We cannot currently serve the page you request";
            errorTech = "Internal error";
            userExceptionDetail = "The current page refuses to load due to an internal error";
            mav.setViewName("/error/500");
            mav.addObject("status", HttpStatus.INTERNAL_SERVER_ERROR.value());
        } else {
            errorHuman = "We cannot serve the current page";
            errorTech = "General error";
            userExceptionDetail = "A generic error prevents from serving the page";
            mav.setViewName("/error/generic");
            mav.addObject("status", response.getStatus());
        }

        Exception userException = new Exception(userExceptionDetail);
        mav.addObject("error_human", errorHuman);
        mav.addObject("error_tech", errorTech);
        mav.addObject("exception", userException);
        return mav;
    }
}

WebAppConfig.java (registers custom exception resolver as an exception handler)

package ...;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.core.env.Environment;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.NoHandlerFoundException;
import org.springframework.web.servlet.view.InternalResourceViewResolver;

import java.lang.ClassNotFoundException;
import java.lang.NullPointerException;
import javax.annotation.Resource;
import javax.ws.rs.InternalServerErrorException;
import java.util.Properties;

@Configuration
@ComponentScan("...")
@EnableWebMvc
@EnableTransactionManagement
@PropertySource("classpath:application.properties")
public class WebAppConfig extends WebMvcConfigurerAdapter {

    @Resource
    private Environment env;

    // ...

    @Bean
    HandlerExceptionResolver customExceptionResolver () {
        CustomSimpleMappingExceptionResolver resolver = new CustomSimpleMappingExceptionResolver();
        Properties mappings = new Properties();
        // Mapping Spring internal error NoHandlerFoundException to a view name
        mappings.setProperty(NoHandlerFoundException.class.getName(), "/error/404");
        mappings.setProperty(InternalServerErrorException.class.getName(), "/error/500");
        mappings.setProperty(NullPointerException.class.getName(), "/error/500");
        mappings.setProperty(ClassNotFoundException.class.getName(), "/error/500");
        mappings.setProperty(Exception.class.getName(), "/error/generic");
        resolver.setExceptionMappings(mappings);
        // Set specific HTTP codes
        resolver.addStatusCode("404", HttpStatus.NOT_FOUND.value());
        resolver.addStatusCode("500", HttpStatus.INTERNAL_SERVER_ERROR.value());
        resolver.setDefaultErrorView("/error/generic");
        resolver.setDefaultStatusCode(200);
        // This resolver will be processed before the default ones
        resolver.setOrder(Ordered.HIGHEST_PRECEDENCE);
        resolver.setExceptionAttribute("exception");
        return resolver;
    }

    // ...

    @Bean
    public InternalResourceViewResolver setupViewResolver() {
        InternalResourceViewResolver resolver = new InternalResourceViewResolver();
        resolver.setPrefix("/WEB-INF/views");
        resolver.setSuffix(".jsp");
        resolver.setExposeContextBeansAsAttributes(true);
        return resolver;
    }

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        super.addViewControllers(registry);
    }
}

Initializer.java (adding dispatcherServlet.setThrowExceptionIfNoHandlerFound(true);; maybe not needed)

package ...;

import org.springframework.web.WebApplicationInitializer;
import org.springframework.web.context.ContextLoaderListener;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRegistration;

public class Initializer implements WebApplicationInitializer {

    public void onStartup(ServletContext servletContext) throws ServletException {
        AnnotationConfigWebApplicationContext ctx = new AnnotationConfigWebApplicationContext();
        ctx.register(WebAppConfig.class);
        servletContext.addListener(new ContextLoaderListener(ctx));
        ctx.setServletContext(servletContext);
        DispatcherServlet dispatcherServlet = new DispatcherServlet(ctx);
        dispatcherServlet.setThrowExceptionIfNoHandlerFound(true);

        // Add the dispatcher servlet mapping manually and make it initialize automatically
        ServletRegistration.Dynamic servlet = servletContext.addServlet("dispatcher", dispatcherServlet);
        servlet.addMapping("/");
        servlet.addMapping("*.png");
        servlet.addMapping("*.jpg");
        servlet.addMapping("*.css");
        servlet.addMapping("*.js");
        servlet.addMapping("*.txt");
        servlet.setLoadOnStartup(1);

        // ...

    }
}

Structure of views and tags related to error classes:

    src/main/webapp/WEB-INF/
    ├── tags
    │   └── error.tag
    └── views
        ├── error
        │   ├── 404.jsp
        │   ├── 500.jsp
        └────── generic.jsp

src/main/webapp/WEB-INF/tags/error.tag

<%@taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>

<!DOCTYPE html>
<head>
    <title>Error page</title>
</head>
<body>
<div class="container">
    <h3><c:out value="${error_human}" /></h3>

    <p><br/><br/></p>

    <div class="panel panel-primary">
        <div class="panel-heading">
            <c:out value="${error_tech}" />
        </div>
        <div class="panel-body">
            <p><c:out value="${exception_message}" /></p>
        </div>
    </div>
</div>
</body>
</html>

src/main/webapp/WEB-INF/views/error/404.jsp

<%@ page language="java" contentType="text/html; charset=utf-8"
         pageEncoding="utf-8" isErrorPage="true" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib tagdir="/WEB-INF/tags/" prefix="g" %>

<c:set var = "error_human" scope = "session" value = "We cannot find the page you are looking for"/>
<c:set var = "error_tech" scope = "session" value = "Page not found"/>
<c:set var = "exception_message" scope = "session" value = "The current page cannot be found"/>
<g:error />

src/main/webapp/WEB-INF/views/error/500.jsp

<%@ page language="java" contentType="text/html; charset=utf-8"
         pageEncoding="utf-8" isErrorPage="true" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib tagdir="/WEB-INF/tags/" prefix="g" %>

<c:set var = "error_human" scope = "session" value = "We cannot currently serve the page you request"/>
<c:set var = "error_tech" scope = "session" value = "Internal error"/>
<c:set var = "exception_message" scope = "session" value = "The current page refuses to load due to an internal error"/>
<g:error />

src/main/webapp/WEB-INF/views/error/generic.jsp

<%@ page language="java" contentType="text/html; charset=utf-8"
         pageEncoding="utf-8" isErrorPage="true" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib tagdir="/WEB-INF/tags/" prefix="g" %>

<c:set var = "error_human" scope = "session" value = "We cannot serve the current page"/>
<c:set var = "error_tech" scope = "session" value = "General error"/>
<c:set var = "exception_message" scope = "session" value = "A generic error prevents from serving the page"/>
<g:error />
Kiddo
  • 158
  • 1
  • 4
  • 12
0

Reading the Spring Boot docs, this works for me:

  @Bean
   public ErrorPageRegistrar errorPageRegistrar() {
     return registry -> registry.addErrorPages(new ErrorPage(HttpStatus.NOT_FOUND, "/index.html"));  }

This is equivalente to web.xml.

0

Make sure you can access 404 page, and then add these codes.

@ControllerAdvice
public class GlobalExceptionHandler {

    @ResponseStatus(HttpStatus.NOT_FOUND)
    @ExceptionHandler(NoHandlerFoundException.class)
    public String handle404(Model model, HttpServletRequest req, Exception ex) {
        return "/404";
    }
}

application.yaml

spring:
  mvc:
    throwExceptionIfNoHandlerFound: true # if page not found, it will throw error, and then ControllerAdvice will catch the error.

PS: springBoot version=2.4.2; Java=15

Jess Chen
  • 3,136
  • 1
  • 26
  • 35