16

I am developing a REST service using SpringMVC, where I have @RequestMapping at class and method level.

This application is currently configured to return error-page jsp configured in web.xml.

<error-page>
    <error-code>404</error-code>
    <location>/resourceNotFound</location>
</error-page>

I however want to return custom JSON instead of this error page.

I am able to handle exception and return json for other exceptions, by writing this in controller, but not sure how and where to write the logic to return JSON when the url does not exist at all.

    @ExceptionHandler(TypeMismatchException.class)
        @ResponseStatus(value=HttpStatus.NOT_FOUND)
        @ResponseBody
        public ResponseEntity<String> handleTypeMismatchException(HttpServletRequest req, TypeMismatchException ex) {

            HttpHeaders headers = new HttpHeaders();
            headers.add("Content-Type", "application/json; charset=utf-8");
            Locale locale = LocaleContextHolder.getLocale();
            String errorMessage = messageSource.getMessage("error.patient.bad.request", null, locale);

            errorMessage += ex.getValue();
            String errorURL = req.getRequestURL().toString();

            ErrorInfo errorInfo = new ErrorInfo(errorURL, errorMessage);
            return new ResponseEntity<String>(errorInfo.toJson(), headers, HttpStatus.BAD_REQUEST);

        }

I tried @ControllerAdvice, it works for other exception scenarios, but not when mapping is not avaialble,

@ControllerAdvice
public class RestExceptionProcessor {

    @Autowired
    private MessageSource messageSource;

    @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
    @ResponseStatus(value=HttpStatus.NOT_FOUND)
    @ResponseBody
    public ResponseEntity<String> requestMethodNotSupported(HttpServletRequest req, HttpRequestMethodNotSupportedException ex) {
        Locale locale = LocaleContextHolder.getLocale();
        String errorMessage = messageSource.getMessage("error.patient.bad.id", null, locale);

        String errorURL = req.getRequestURL().toString();

        ErrorInfo errorInfo = new ErrorInfo(errorURL, errorMessage);
        return new ResponseEntity<String>(errorInfo.toJson(), HttpStatus.BAD_REQUEST);
    }

    @ExceptionHandler(NoSuchRequestHandlingMethodException.class)
    @ResponseStatus(value=HttpStatus.NOT_FOUND)
    @ResponseBody
    public ResponseEntity<String> requestHandlingMethodNotSupported(HttpServletRequest req, NoSuchRequestHandlingMethodException ex) {
        Locale locale = LocaleContextHolder.getLocale();
        String errorMessage = messageSource.getMessage("error.patient.bad.id", null, locale);

        String errorURL = req.getRequestURL().toString();

        ErrorInfo errorInfo = new ErrorInfo(errorURL, errorMessage);
        return new ResponseEntity<String>(errorInfo.toJson(), HttpStatus.BAD_REQUEST);
    }


}
Himalay Majumdar
  • 3,883
  • 14
  • 65
  • 94

4 Answers4

25

After digging around DispatcherServlet and HttpServletBean.init() in SpringFramework I see that its possible in Spring 4.

org.springframework.web.servlet.DispatcherServlet

/** Throw a NoHandlerFoundException if no Handler was found to process this request? **/
private boolean throwExceptionIfNoHandlerFound = false;

protected void noHandlerFound(HttpServletRequest request, HttpServletResponse response) throws Exception {
    if (pageNotFoundLogger.isWarnEnabled()) {
        String requestUri = urlPathHelper.getRequestUri(request);
        pageNotFoundLogger.warn("No mapping found for HTTP request with URI [" + requestUri +
                "] in DispatcherServlet with name '" + getServletName() + "'");
    }
    if(throwExceptionIfNoHandlerFound) {
        ServletServerHttpRequest req = new ServletServerHttpRequest(request);
        throw new NoHandlerFoundException(req.getMethod().name(),
                req.getServletRequest().getRequestURI(),req.getHeaders());
    } else {
        response.sendError(HttpServletResponse.SC_NOT_FOUND);
    }
}

throwExceptionIfNoHandlerFound is false by default and we should enable that in web.xml

<servlet>
    <servlet-name>appServlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>throwExceptionIfNoHandlerFound</param-name>
            <param-value>true</param-value>
        </init-param>
    <load-on-startup>1</load-on-startup>
    <async-supported>true</async-supported>
</servlet>

And then you can catch it in a class annotated with @ControllerAdvice using this method.

@ExceptionHandler(NoHandlerFoundException.class)
@ResponseStatus(value=HttpStatus.NOT_FOUND)
@ResponseBody
public ResponseEntity<String> requestHandlingNoHandlerFound(HttpServletRequest req, NoHandlerFoundException ex) {
    Locale locale = LocaleContextHolder.getLocale();
    String errorMessage = messageSource.getMessage("error.bad.url", null, locale);

    String errorURL = req.getRequestURL().toString();

    ErrorInfo errorInfo = new ErrorInfo(errorURL, errorMessage);
    return new ResponseEntity<String>(errorInfo.toJson(), HttpStatus.BAD_REQUEST);
}

Which allows me to return JSON response for bad URLs for which no mapping exist, instead of redirecting to a JSP page :)

{"message":"URL does not exist","url":"http://localhost:8080/service/patientssd"}
Himalay Majumdar
  • 3,883
  • 14
  • 65
  • 94
  • 3
    Excellent Answer, it's also worth noting that you can't use [DefaultServletHandlerConfigurer](http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/servlet/config/annotation/DefaultServletHandlerConfigurer.html) with this as that will consume the 404 before it gets to the DispatcherServlet – muttonUp Sep 17 '14 at 00:44
  • i have tried this way, i hope a resolver that implements HandlerExceptionResolver will catch this exception but it's not. – acerphenix Jan 30 '16 at 14:25
  • 3
    For java-based configuration, you need to override the `customizeRegistration` of the `AbstractAnnotationConfigDispatcherServletInitializer`. See [this answer](http://stackoverflow.com/a/22751886/878514). – fracz Apr 19 '16 at 06:54
  • throwExceptionIfNoHandlerFound in web.xml solved the issue for me. Dispatcher servlet now throws the error needed for my @ControllerAdvice to handle it gracefully – bashar Apr 10 '17 at 07:08
  • Using the above approach, I am still encountering NoHandlerFoundException in RestExceptionProcessor class. – Siddharth.Singh Nov 04 '20 at 19:26
15

If you are using Spring Boot, set BOTH of these two properties:

spring.resources.add-mappings=false
spring.mvc.throw-exception-if-no-handler-found=true

Now your @ControllerAdvice annotated class can handle the "NoHandlerFoundException", as below.

@ControllerAdvice
@RequestMapping(produces = "application/json")
@ResponseBody
public class RestControllerAdvice {

    @ExceptionHandler(NoHandlerFoundException.class)
    public ResponseEntity<Map<String, Object>> unhandledPath(final NoHandlerFoundException e) {
        Map<String, Object> errorInfo = new LinkedHashMap<>();
        errorInfo.put("timestamp", new Date());
        errorInfo.put("httpCode", HttpStatus.NOT_FOUND.value());
        errorInfo.put("httpStatus", HttpStatus.NOT_FOUND.getReasonPhrase());
        errorInfo.put("errorMessage", e.getMessage());
        return new ResponseEntity<Map<String, Object>>(errorInfo, HttpStatus.NOT_FOUND);
    }

}

note it is not sufficient to only specify this property:

spring.mvc.throw-exception-if-no-handler-found=true

, as by default Spring maps unknown urls to /**, so there really never is "no handler found".

To disable the unknown url mapping to /**, you need

spring.resources.add-mappings=false ,

which is why the two properties together produce the desired behavior.

mancini0
  • 4,285
  • 1
  • 29
  • 31
  • Property `spring.resources.add-mappings` is deprecated. You can replace it with `spring.web.resources.add-mappings` – pbaris Mar 24 '22 at 16:03
-1

If you're using spring 3.2 or later you can use a controller advice (@ControllerAdvice) to deal with, amongst other things, mapping errors (404's). You can find documentation here. Take a look at section 17.11. You can use this, for example, to provide more detailed logging on why your request bindings aren't being matched for specific urls, or to simply return a more specific response than a generic 404.

Software Engineer
  • 15,457
  • 7
  • 74
  • 102
  • What spring MVC version are you using? – Sotirios Delimanolis Mar 04 '14 at 01:15
  • I followed your suggestion and implemented @Controller Advice using HttpRequestMethodNotSupportedException and NoSuchRequestHandlingMethodException, I still get redirected to jsp when I am expecting JSON. The controller advice works for other scenarios however. Updated my code. I am using Spring 3.2 and I am ok to upgrade. – Himalay Majumdar Mar 04 '14 at 20:25
-4

you can return json in the location below,that /handle/404.

<error-page>
    <error-code>404</error-code>
    <location>/handle/404</location>
</error-page>

after you config this in web.xml,a 404 error will redirect to /handle/404,and you can create a controller with this mapping and return a json result. for example.

@RestController
@RequestMapping(value = "handle")
public class HttpErrorController {
   @RequestMapping(value = "404")
   public String handle404() {
      return "404 error";
   }
}
acerphenix
  • 313
  • 3
  • 6