0

I have to return customized error structures in the REST API I'm developping using springboot 2.0.4.RELEASE. I developped the controller advice below that does the work ... except when a client sends a request that contains illegal characters. For example: http://mycompany/myapp/myservice?fields=v1[*] ([*] should have been url encoded).

First time a client sends such a request, I got this message into the log

2018-09-11 10:41:44.986 INFO 11464 --- [io-8080-exec-10] o.apache.coyote.http11.Http11Processor : Error parsing HTTP request header Note: further occurrences of HTTP header parsing errors will be logged at DEBUG level.

java.lang.IllegalArgumentException: Invalid character found in the request target. The valid characters are defined in RFC 7230 and RFC 3986 at org.apache.coyote.http11.Http11InputBuffer.parseRequestLine(Http11InputBuffer.java:479) ~[tomcat-embed-core-8.5.32.jar:8.5.32] at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:684) ~[tomcat-embed-core-8.5.32.jar:8.5.32] at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66) [tomcat-embed-core-8.5.32.jar:8.5.32] at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:800) [tomcat-embed-core-8.5.32.jar:8.5.32] at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1471) [tomcat-embed-core-8.5.32.jar:8.5.32] at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) [tomcat-embed-core-8.5.32.jar:8.5.32] at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) [na:1.8.0_144] at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) [na:1.8.0_144] at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) [tomcat-embed-core-8.5.32.jar:8.5.32] at java.lang.Thread.run(Thread.java:748) [na:1.8.0_144]

... but none of my controller advice's method are called and I get an http error 400 (that's fine), with an empty body (that's wrong). Following time, same except nothing is ouput to log.

What should I do to catch this error and return my custom error structure?

Here is the (simplified code of my Controller advice):

package com.renault.api.examples.springboot;

import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.ConversionNotSupportedException;
import org.springframework.beans.TypeMismatchException;
import org.springframework.boot.web.servlet.error.DefaultErrorAttributes;
import org.springframework.boot.web.servlet.error.ErrorAttributes;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.validation.BindException;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingPathVariableException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.context.request.async.AsyncRequestTimeoutException;
import org.springframework.web.multipart.support.MissingServletRequestPartException;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.NoHandlerFoundException;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;

import com.fasterxml.jackson.databind.ObjectMapper;

@ControllerAdvice
public class ExceptionHandler extends ResponseEntityExceptionHandler {
    public static final class CustomError {
        // custom attributes removed for publication
    }

    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
    private static final Logger LOGGER = LoggerFactory.getLogger(ExceptionHandler.class);
    private static final String UNEXPECTED_EXCEPTION_MESSAGE = "An unexpected exception occurred";

    @Override
    protected ResponseEntity<Object> handleTypeMismatch(TypeMismatchException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
System.out.println("handleTypeMismatch");
        return new ResponseEntity<Object>(new CustomError(), headers, status);
    }

    @Override
    protected ResponseEntity<Object> handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException ex,
            HttpHeaders headers, HttpStatus status, WebRequest request) {
System.out.println("handleHttpRequestMethodNotSupported");
        return super.handleHttpRequestMethodNotSupported(ex, headers, status, request);
    }
    @Override
    protected ResponseEntity<Object> handleHttpMediaTypeNotSupported(HttpMediaTypeNotSupportedException ex,
            HttpHeaders headers, HttpStatus status, WebRequest request) {
System.out.println("handleHttpMediaTypeNotSupported");
        return super.handleHttpMediaTypeNotSupported(ex, headers, status, request);
    }
    @Override
    protected ResponseEntity<Object> handleHttpMediaTypeNotAcceptable(HttpMediaTypeNotAcceptableException ex,
            HttpHeaders headers, HttpStatus status, WebRequest request) {
System.out.println("handleHttpMediaTypeNotAcceptable");
        return super.handleHttpMediaTypeNotAcceptable(ex, headers, status, request);
    }
    @Override
    protected ResponseEntity<Object> handleMissingPathVariable(MissingPathVariableException ex, HttpHeaders headers,
            HttpStatus status, WebRequest request) {
System.out.println("handleMissingPathVariable");
        return super.handleMissingPathVariable(ex, headers, status, request);
    }
    @Override
    protected ResponseEntity<Object> handleMissingServletRequestParameter(MissingServletRequestParameterException ex,
            HttpHeaders headers, HttpStatus status, WebRequest request) {
System.out.println("handleMissingServletRequestParameter");
        return super.handleMissingServletRequestParameter(ex, headers, status, request);
    }
    @Override
    protected ResponseEntity<Object> handleServletRequestBindingException(ServletRequestBindingException ex,
            HttpHeaders headers, HttpStatus status, WebRequest request) {
System.out.println("handleServletRequestBindingException");
        return super.handleServletRequestBindingException(ex, headers, status, request);
    }
    @Override
    protected ResponseEntity<Object> handleConversionNotSupported(ConversionNotSupportedException ex,
            HttpHeaders headers, HttpStatus status, WebRequest request) {
System.out.println("handleConversionNotSupported");
        return super.handleConversionNotSupported(ex, headers, status, request);
    }
    @Override
    protected ResponseEntity<Object> handleHttpMessageNotReadable(HttpMessageNotReadableException ex,
            HttpHeaders headers, HttpStatus status, WebRequest request) {
System.out.println("handleHttpMessageNotReadable");
        return super.handleHttpMessageNotReadable(ex, headers, status, request);
    }
    @Override
    protected ResponseEntity<Object> handleHttpMessageNotWritable(HttpMessageNotWritableException ex,
            HttpHeaders headers, HttpStatus status, WebRequest request) {
System.out.println("handleHttpMessageNotWritable");
        return super.handleHttpMessageNotWritable(ex, headers, status, request);
    }
    @Override
    protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex,
            HttpHeaders headers, HttpStatus status, WebRequest request) {
System.out.println("handleMethodArgumentNotValid");
        return super.handleMethodArgumentNotValid(ex, headers, status, request);
    }
    @Override
    protected ResponseEntity<Object> handleMissingServletRequestPart(MissingServletRequestPartException ex,
            HttpHeaders headers, HttpStatus status, WebRequest request) {
System.out.println("handleMissingServletRequestPart");
        return super.handleMissingServletRequestPart(ex, headers, status, request);
    }
    @Override
    protected ResponseEntity<Object> handleBindException(BindException ex, HttpHeaders headers, HttpStatus status,
            WebRequest request) {
System.out.println("handleBindException");
        return super.handleBindException(ex, headers, status, request);
    }
    @Override
    protected ResponseEntity<Object> handleNoHandlerFoundException(NoHandlerFoundException ex, HttpHeaders headers,
            HttpStatus status, WebRequest request) {
System.out.println("handleNoHandlerFoundException");
        return super.handleNoHandlerFoundException(ex, headers, status, request);
    }
    @Override
    protected ResponseEntity<Object> handleAsyncRequestTimeoutException(AsyncRequestTimeoutException ex,
            HttpHeaders headers, HttpStatus status, WebRequest webRequest) {
System.out.println("handleAsyncRequestTimeoutException");
        return super.handleAsyncRequestTimeoutException(ex, headers, status, webRequest);
    }
    @Override
    protected ResponseEntity<Object> handleExceptionInternal(Exception ex, Object body, HttpHeaders headers,
            HttpStatus status, WebRequest request) {
System.out.println("handleExceptionInternal");
        return super.handleExceptionInternal(ex, body, headers, status, request);
    }

    /** Handles all exceptions not processed by spring boot thrown during request processing.
     * <br>this means the errors like "path not found", "wrong argument" are not processed by this method. 
     * @param ex The exception that occurs.
     * @param request The request that sends the exception.
     * @return The response to send to client. For publication, all exceptions are considered unexpected => http error 500
     */
    @org.springframework.web.bind.annotation.ExceptionHandler(value= {Exception.class})
    protected ResponseEntity<Object> handle(Exception ex, WebRequest request) {
        LOGGER.error(UNEXPECTED_EXCEPTION_MESSAGE, ex);
        CustomError response = new CustomError();
        // Fill custom error attributes ... removed for publication
        return handleExceptionInternal(ex, response, new HttpHeaders(), HttpStatus.INTERNAL_SERVER_ERROR, request);
    }

    /** Handles "standard" errors, like "path not found".
     * @return an ErrorAttributes object describing the attributes of the response's body.
     */
    @Bean
    public ErrorAttributes errorAttributes() {
        return new DefaultErrorAttributes() {
            @Override
            public int getOrder() {
System.out.println("getOrder is called");
                return super.getOrder();
            }

            @Override
            public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response,
                    Object handler, Exception ex) {
System.out.println("resolveException is called");
                return super.resolveException(request, response, handler, ex);
            }

            @Override
            public Throwable getError(WebRequest webRequest) {
System.out.println("getError is called");
                return super.getError(webRequest);
            }

            @Override
            public Map<String, Object> getErrorAttributes(WebRequest request, boolean includeStackTrace) {
System.out.println("getErrorAttributes is called");
                CustomError err = new CustomError();
                // Reuse default attributes to fill custom error ... removed for publication
                @SuppressWarnings("unchecked")
                Map<String, Object> map = OBJECT_MAPPER.convertValue(err, Map.class);
                return map;
            }
        };
    }
}

Thanks

Jean-Marc Astesana
  • 1,242
  • 3
  • 16
  • 24
  • I think you should try: >https://stackoverflow.com/questions/33386479/spring-mvc-custom-message-for-http-400#2 – ktcl Jan 04 '19 at 04:41
  • Not sure to understand. The link talk about ControllerAdvice ... which is exactly what I tried. – Jean-Marc Astesana Jan 04 '19 at 06:24
  • Hi, did you find a nice solution for this? There is a somewhat similar issue with exceptions thrown by StrictHttpFirewall, and therefore occurring before we enter the scope of a Controller (and its Advice), this one here occurs yet earlier, before we're even in the domain of a spring app. So I believe it should be tackled with an old-school Filter, but I'd be curious to know if you came up with something – A D Dec 18 '20 at 16:05
  • Hi, Unfortunately, I found no solution :-( – Jean-Marc Astesana Dec 22 '20 at 09:20

0 Answers0