0

My handleSecurityExceptions() method always returns a HTTP error status of 500 to the client when not requesting JSON (such as when requesting HTML). I am making a wild guess this is because I'm doing something wrong and causing another internal exception that's being swallowed but could be way off base. Uncommenting the commented out lines in ApplicationResponseEntityExceptionHandler provides a work-around to get rid of the issue but what's the right way of doing what I'm doing below? Presumably there's a proper way to get spring to format MessageResponse?

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
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.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;

@ControllerAdvice
public class ApplicationResponseEntityExceptionHandler
        extends ResponseEntityExceptionHandler {
    static final Logger logger = LoggerFactory.getLogger(ApplicationResponseEntityExceptionHandler.class);

    private Boolean canAcceptJson(WebRequest request) {
        boolean canAccept = false;
        String[] accepts = request.getHeaderValues("accept");
        for (String value : (accepts == null ? new String[]{} : accepts)) {
            for (String directive : value.toLowerCase().split(",")) {
                directive=directive.trim();
                canAccept = directive.toLowerCase().startsWith("application/json") && (directive.length() == 16 || directive.charAt(16) == ',' || directive.charAt(16) == ';');
                if (canAccept)
                    break;
            }
        }
        return canAccept;
    }

    @ExceptionHandler(value
            = {ApplicationSecurityException.class})
    protected ResponseEntity<Object> handleSecurityExceptions(
            ApplicationSecurityException ex, WebRequest request) {
        logger.info("Security exception {}", ex.getMessage());
//        if (canAcceptJson(request))
            return handleExceptionInternal(ex, new MessageResponse(ex.getMessage()),
                    new HttpHeaders(), HttpStatus.UNAUTHORIZED, request);
//        else
//            return handleExceptionInternal(ex, ex.getMessage(),
//                    new HttpHeaders(), HttpStatus.UNAUTHORIZED, request);

    }
}
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;

@AllArgsConstructor
public class MessageResponse {
    @Getter
    @Setter
    private String message;

    public String toString() {
        return message;
    }
}
import lombok.Getter;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
public class ApplicationSecurityException extends RuntimeException {
    @Getter
    final String message;
}
Rob Pitt
  • 354
  • 3
  • 10

1 Answers1

0

After setting off an exception to discover where the processing was done, much poking around on the source code, numerous different solutions each with trade-offs and a bit googling I found two links that helped a fair bit here and here. I came up with this. Might be able to do something similar with an ErrorController and extending DefaultErrorAttributes instead of an @ExceptionHandler but I hadn't learned enough about how the Spring Boot framework renders it's content when I was attempting that solution and I'm not going to try it just now for the sake of it when I have a relatively tidy solution and working code. Here's what I came up with, it still needs a bit of refactoring and flushing out so it sends the right HTTP status for the (sub)exception type rather than UNAUTHORIZED for all but it works:

@ControllerAdvice
public class ApplicationResponseEntityExceptionHandler
        extends ResponseEntityExceptionHandler {

    @Autowired
    Environment environment;
    @Autowired
    ViewResolver viewResolver;
    @Autowired
    ContentNegotiationManager contentNegotiationManager;

    static final Logger logger = LoggerFactory.getLogger(ApplicationResponseEntityExceptionHandler.class);

    @ExceptionHandler(ApplicationException.class)
    protected void handleApplicationExceptions(
            ApplicationException ex, HttpServletRequest servletRequest, Model model, HttpServletResponse servletResponse) throws Exception {
        if (ex instanceof ApplicationSecurityException) {
            logger.info("Security exception at {} from {} ({})", servletRequest.getRequestURL(), servletRequest.getRemoteAddr(), ex.getMessage());

        } else {
            logger.debug("Application exception at {} from {} ({})", servletRequest.getRequestURL(), servletRequest.getRemoteAddr(), ex.getMessage());
        }

        // This section of the code checks if we should send HTML or JSON as a response.
        List<MediaType> mediaTypes;
        MediaType htmlType = new MediaType("text","html");
        // Only supporting JSON as an alternative content type for these exceptions.
        MediaType jsonType = new MediaType( "application", "json");
        boolean wantHtml = true;
        try {
            mediaTypes = contentNegotiationManager.resolveMediaTypes(new ServletWebRequest(servletRequest));
            MediaType.sortBySpecificityAndQuality(mediaTypes);
        } catch (HttpMediaTypeNotAcceptableException e) {
            logger.debug("resolveMediaTypes() threw exception");
            throw ex; // Let the framework handle this.
        }
        for (MediaType mediaType : mediaTypes) {
            logger.debug(mediaType.toString());
            if (mediaType.isCompatibleWith(htmlType)) {
                break;
            } else if (mediaType.isCompatibleWith(jsonType)) {
                wantHtml = false;
                break;
            }
        }

        if (wantHtml) {
            // This section renders an error template if HTML is the most appropriate response
            boolean developmentExceptionView = false;
            for(String profile : environment.getActiveProfiles()) {
                if (profile.equals("development")) {
                    developmentExceptionView = true;
                    break;
                }
            }
            View resolvedView = viewResolver.resolveViewName(developmentExceptionView ? "error-development" : "error-production", Locale.ROOT);
            if (resolvedView == null) {
                // TODO: Do something else
                throw ex;
            }
            if (developmentExceptionView) {
                StringWriter sw = new StringWriter();
                PrintWriter pw = new PrintWriter(sw);
                ex.printStackTrace(pw);
                model.addAttribute("exception", ex.getClass().getName());
                model.addAttribute("trace", sw.toString());
                model.addAttribute("message", ex.getLocalizedMessage());
            }
            servletResponse.setStatus(HttpStatus.UNAUTHORIZED.value());
            resolvedView.render(model.asMap(), servletRequest, servletResponse);
        } else {
            // This section renders JSON if the client requested it in preference to HTML
            // ReturnValue could be any JSON serialisable object so code not provided
            ExceptionWithCodeResponse returnValue = new ExceptionWithCodeResponse(ex.getMessage(), ex.getCode());
            Constructor<?>[] constructors = returnValue.getClass().getConstructors();
            ServletWebRequest nativeRequest = new ServletWebRequest(servletRequest, servletResponse);
            List<HttpMessageConverter<?>> messageConverters;
            messageConverters = new ArrayList<HttpMessageConverter<?>>();
            messageConverters.add(new MappingJackson2HttpMessageConverter());
            RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(messageConverters);
            ModelAndViewContainer mavContainer = new ModelAndViewContainer();
            servletResponse.setStatus(HttpStatus.UNAUTHORIZED.value());
            processor.handleReturnValue(returnValue,
                    new MethodParameter(constructors[0], -1), mavContainer, nativeRequest);
        }
    }
}
Rob Pitt
  • 354
  • 3
  • 10