Spring Boot error handling in app combining MVC and rest api - java

I am currently trying to figure out the best way to handle errors in a spring boot app.
Assume that the app consists of thymeleaf templates served on paths starting with /admin, and a REST API served on other URIs.
My project currently has the following configuration:
Thymeleaf error templates located at /src/main/resources/templates/error/{id}.html, defined for errors 400, 401, 403, 404, 500.
Default thymeleaf error template /src/main/resources/templates/error.html
HttpSecurity configuration:
#Override
protected void configure(HttpSecurity http) throws Exception {
(...)
http
.anonymous();
http
.exceptionHandling()
.authenticationEntryPoint((request, response, authException) -> response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase()))
.accessDeniedHandler(new AccessDeniedHandlerImpl());
(...)
}
Exception Handlers for both rest and MVC:
#ControllerAdvice
public class ExceptionHandlers {
private final BasicErrorController basicErrorController;
public ExceptionHandlers(BasicErrorController basicErrorController) {
this.basicErrorController = basicErrorController;
}
#ExceptionHandler(Exception.class)
public Object handleAllExceptions(Exception e, HttpServletRequest request, HttpServletResponse response) {
return handle(e, request, response, HttpStatus.INTERNAL_SERVER_ERROR);
}
#ExceptionHandler(EntityNotFoundException.class)
public Object handleEntityNotFoundException(EntityNotFoundException e, HttpServletRequest request, HttpServletResponse response) {
return handle(e, request, response, HttpStatus.NOT_FOUND, I18nCodes.ENTITY_NOT_FOUND);
}
/**
* We exclude all exceptions deriving from {#link AccessDeniedException} from custom exception handling.
*/
#ExceptionHandler(AccessDeniedException.class)
public Object handleAccessDeniedException(AccessDeniedException e, HttpServletRequest request, HttpServletResponse response) {
throw e;
}
/**
* We exclude all exceptions deriving from {#link AppBaseException} from custom exception handling.
*/
#ExceptionHandler(AppBaseException.class)
public Object handleAppBaseException(AppBaseException e, HttpServletRequest request, HttpServletResponse response) {
throw e;
}
private Object handle(Exception e, HttpServletRequest request, HttpServletResponse response, HttpStatus status) {
return handle(e, request, response, HttpStatus.INTERNAL_SERVER_ERROR, I18nCodes.getCodeByStatus(status));
}
private Object handle(Exception e, HttpServletRequest request, HttpServletResponse response, HttpStatus status, String message) {
String header = request.getHeader("Accept");
if (header != null && header.contains("text/html")) {
setErrorCode(request, response, status);
return basicErrorController.errorHtml(request, response);
}
return createJsonResponse(message, status, request.getRequestURI());
}
private ResponseEntity<ErrorResponseDTO> createJsonResponse(String message, HttpStatus status, String path) {
ErrorResponseDTO errorResponseDTO = new ErrorResponseDTO()
.setTimestamp(new Timestamp(System.currentTimeMillis()))
.setStatus(status.value())
.setMessage(message)
.setPath(path)
.setError(status.name().toLowerCase());
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setContentType(MediaType.APPLICATION_JSON);
return ResponseEntity.status(status).headers(httpHeaders).body(errorResponseDTO);
}
private void setErrorCode(HttpServletRequest request, HttpServletResponse response, HttpStatus httpStatus) {
request.setAttribute(RequestDispatcher.ERROR_STATUS_CODE, httpStatus.value());
response.setStatus(httpStatus.value());
}
}
Without the Exception handlers everything works about as expected, I get 401 error when I am not authenticated, and 403 when authenticated but lacking authorities. My custom exceptions are correctly mapped according to their message and Status defined in #ResponseStatus eg:
#ResponseStatus(value = HttpStatus.BAD_REQUEST)
public class AccountInfoException extends AppBaseException {
protected AccountInfoException(String message) {
super(message);
}
protected AccountInfoException(String message, Throwable cause) {
super(message, cause);
}
public static AccountInfoException emailAlreadyExists() {
return new AccountInfoException(I18nCodes.EMAIL_EXIST);
}
public static AccountInfoException accountNotFound() {
return new AccountInfoException(I18nCodes.ACCOUNT_NOT_FOUND);
}
}
Will map to status 400 with one of two messages I18nCodes.EMAIL_EXIST or I18nCodes.ACCOUNT_NOT_FOUND.
I arrive on the correct thymeleaf error page depending on the status code - life is perfect.
The issue comes with the requirement of handling all other exceptions. It's not a possibility to let an unexpected error to the client. So for that I've defined an exception handler handing Exception, to return a generic 500 message. This breaks everything. I'm unable to access the Status codes of my custom exceptions
Authentication entry point and access denied handler are both ignored, and both cases of being not authenticated and having insufficient authority are handles in the same exception(AccessDeniedException). I also lose default mappings for some exceptions eg. org.springframework.security.authentication.LockedException returns status 401.
My temporary workaround was to handle all exceptions, and to create methods handling exceptions that I want to ignore and just rethrow the exception. I feel like the best thing I could do is remove all exception handlers and repack all container exceptions to my own custom exceptions, only issue is that there doesn't seem to be a way to do it.
I'm looking for a more permanent solution - what would be the best approach?

Related

Can I include the restTemplate request body inside restTemplate.setErrorHandler?

Here In the below simple code of RestTemplate's errorHandler I want to include the request body inside the log
I want to keep the code clean and not using the error handler out side restTemplate configuration class. So the only way i found is to handel it from rest template caller/user, WDYT?
#Configuration
public class HTTPConfiguration {
...
restTemplate.setErrorHandler(new DefaultResponseErrorHandler() {
#Override
public void handleError(ClientHttpResponse response) throws IOException {
if (response.getStatusCode().series() == HttpStatus.Series.SERVER_ERROR) {
LOGGER.error("Server error: {} {}", response.getStatusCode(), response.getStatusText());
} else if (response.getStatusCode().series() == HttpStatus.Series.CLIENT_ERROR) {
LOGGER.error("Client error: {} {}", response.getStatusCode(), response.getStatusText());
}
}
#Override
public boolean hasError(ClientHttpResponse response) throws IOException {
return (response.getStatusCode().series() == HttpStatus.Series.CLIENT_ERROR
|| response.getStatusCode().series() == HttpStatus.Series.SERVER_ERROR);
}
});
...
}
The straight answer is no, ResponseErrorHandler is an interface and only has three methods, the best you can get is request url and request method docs
handleError(ClientHttpResponse response)
Handle the error in the given response.
handleError(java.net.URI url, HttpMethod method, ClientHttpResponse response)
Alternative to handleError(ClientHttpResponse) with extra information providing access to the request URL and HTTP method.
handleError(java.net.URI url, HttpMethod method, ClientHttpResponse response)
Indicate whether the given response has any errors.
And ResponseErrorHandler has two implementation classes DefaultResponseErrorHandler, ExtractingResponseErrorHandler but none of them has method with HttpEntity as an argument DefaultResponseErrorHandler,and ExtractingResponseErrorHandler

How do I return a custom http status code using a spring filter?

I'm trying to catch a custom auth exception thrown in the spring security flow (for an expired password, which means the user can use the current credentials to create a new password, but can't do anything else with them) and throw a http status code 419 (not part of the http spec, but easier for my clients to handle). I can't put it in a controller advice class with an ExceptionHandler because that only catches exceptions from the Rest Controller.
I'm able to catch the exception in a high precedence custom Filter, but response.setStatus(419) seems to set it to 401.
Created a custom filter implementing the filter interface, catch the custom exception thrown by the filter chain and try to change the response status
#Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
System.out.println("filtering...");
HttpServletResponse response = (HttpServletResponse) res;
try {
chain.doFilter(req, response);
} catch (ExpiredCredentialsException e) {
System.out.println("Expired credentials exception caught in filter");
response.setStatus(419);
int status = response.getStatus();
System.out.println("status " + status);
}
}
I expected it to print and return 419, but prints/returns 401. Is there something I'm doing wrong or another way to achieve what I want?

Spring interceptor not working for partial REST endpoints

I have a Spring Boot application with REST endpoints defined like this
/usermanagement/v1/access/ldap
/usermanagement/v1/access/db
I have created a Spring Interceptor to intercept all incoming request with following pattern
#Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new RequestInterceptor()).addPathPatterns("/usermanagement/v1/**");
}
RequestInterceptor
#Component
public class RequestInterceptor extends HandlerInterceptorAdapter {
#Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
return true;
}
#Override
public void postHandle(
HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)
throws Exception {
System.out.println("This is Post Handling the request");
}
}
This interceptor works only if client accesses the complete endpoint i.e
/usermanagement/v1/access/ldap
In case a partial endpoint is accessed,
/usermanagement/v1/access
interceptor is not called and a 404 is returned to the client.
Is there a way to change this behavior? The reason I am doing this is because I don't want to expose specific endpoints but common endpoints and make internal calls to services and return result through common endpoints.
You are using the wrong method. Try using afterCompletion instead of postHandle
new HandlerInterceptor() {
#Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//This is called before handeling any request
}
#Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
//This is called after successfully handeling a request. It will not be called in case of an exception
}
#Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//This will always be called after a request, even in case of an exception
}
}
With a request to a undefined endpoint /usermanagement/v1/access Spring will throw an exception. Therefor it never will enter postHandle.

Spring secure filter to protect anonymous requests

One more question about spring configuration...
I have several rest methods opened to everyone and not secured. These rest methods on server #1 are used by another server #2 in the same domain to get some data. The idea is that server #2 sets my_super_secure_cookie to some secure token and server #1 decodes and verifies it. Here is the code:
#Configuration
class SecurityConfig extends WebSecurityConfigurerAdapter {
// Some code
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/rest/public/*").permitAll()
.anyRequest().authenticated();
}
// More code
}
public class SuperSecurityFilter extends FilterSecurityInterceptor implements Filter {
public SuperSecurityFilter(String key) {
super(key);
}
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse res = (HttpServletResponse) response;
Cookie[] cookies = req.getCookies();
Optional<Cookie> tokenCookie = Arrays.stream(cookies).filter(cookie -> cookie.getName().equals("my_super_secure_cookie")).findFirst();
if (tokenCookie.isPresent()) {
Cookie cookie = tokenCookie.get();
TokenCookie.create(cookie.getValue()).validate();
} else {
throw new Exception("Ooops!"));
}
chain.doFilter(req, res);
}
}
The question is how do I configure SecurityConfig to use SecurityTokenFilter on request to any of the /rest/public/* rest methods. Something like:
http
.antMatcher("/rest/public/*")
.addFilterBefore(new SuperSecurityFilter());
is not working, SuperSecurityFilter is not called on request.
p.s. I'm forced to work with this type of security model due to current business logic restrictions.
I solved (applied workaround?) the issue I have by implementing not filter, but interceptor, like this:
public class SuperSecurityInterceptor implements HandlerInterceptor {
#Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// implementation here
}
#Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
// Nothing here
}
#Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// Nothing here
}
}
And registered this interceptor in my entity extending WebMvcConfigurerAdapter. Like this:
registry.addInterceptor(new SupeSecurityInterceptor()).addPathPatterns("/rest/public/*");
Not sure if this is right thing to do though... Anyway would be glad to know about the conventional approach of implementing this type of functionality.

response.sendError throws exception with custom filter

I have a signup form that upon submit I need to:
Create a new user and signin the user.
If the username is already in the database I need to return 409 with error message.
I've created a custom filter which sets the HttpStatus
public class UserSignupFilter implements Filter {
...
#Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
if (userSignup(request)) {
if (userAlreadyExists(request)){
logger.info("Didnt create user since: "+e.getMessage());
response.sendError(HttpStatus.CONFLICT.value());
} else {
createUser(request);
}
}
chain.doFilter(request, response);
}
}
The problem is that I am getting sometimes:
java.lang.IllegalStateException: Cannot call sendError() after the response has been committed.
Is there a better way to do it?
I am setting my security programmatically (Spring Security V 3.2) how can I use ExceptionTranslationFilter?
I don't think you should be calling:
chain.doFilter(request, response);
If all you want to do is send error code back. Just call return
logger.info("Didnt create user since: "+e.getMessage());
response.sendError(HttpStatus.CONFLICT.value());
return;
If you want to use the ExceptionTranslationFilter you need to throw an exception (not commit the response). As long as your filter is downstream if the security filter that should do it, or else put your filter in the security filter chain after the exception translation filter.

Categories

Resources