0

I want to create custom exception handler which returns a structured JSON response with data. I tried this:

@ExceptionHandler({ AccessDeniedException.class })
public ResponseEntity<ErrorResponseDTO> accessDeniedExceptionHandler(final AccessDeniedException ex) {
    ErrorDetail errorDetail = ErrorDetail.NOT_FOUND;

    LOG.error(ex.getMessage(), ex.getCause());
    ErrorResponse errorEntry = new ErrorResponse();
    .......

    return new ResponseEntity<ErrorResponseDTO>(errorResponse, HttpStatus.FORBIDDEN);
}

Full code: Github

But I get this generic result:

{
  "timestamp": "2020-06-06T22:46:13.815+00:00",
  "status": 403,
  "error": "Forbidden",
  "message": "",
  "path": "/engine/users/request"
}

I want to get only this result:

{
  "errors": [{
    "status": 404,
    "code": "1000",
    "title": "Forbidden",
    "detail": "Forbidden",
    "extra": {
      "detail": "Forbidden"
    }
  }]
}

Do you know how I can send custom result and how I can solve this issue?

Peter Penzov
  • 1,126
  • 134
  • 430
  • 808
  • Does this answer your question? [Spring security - creating 403 Access denied custom response](https://stackoverflow.com/questions/48306302/spring-security-creating-403-access-denied-custom-response) – Madhu Bhat Jun 07 '20 at 12:24
  • In my case I want to use Java Class with annotation `@ExceptionHandler({ AccessDeniedException.class })` because I want to store the values into Enum. Is it possible to do this into my handler? – Peter Penzov Jun 07 '20 at 12:36
  • I highly doubt that it would be possible to do with an `@ExceptionHandler`. The reason being, the error responses are handled differently with Spring security and it would never come to the ExceptionHandler, as the response is returned elsewhere. – Madhu Bhat Jun 07 '20 at 12:55
  • It might be possible to throw the exception in the custom AccessDeniedHandler implementation so that it comes to the ExceptionHandler, but that would not be a great way to handle responses. – Madhu Bhat Jun 07 '20 at 12:56
  • Can you show me some example please? – Peter Penzov Jun 07 '20 at 12:59
  • **@ExceptionHandler** annotated method is only active for that particular Controller, not globally for the entire application. And let's say you have multiple controllers, So adding it to every controller makes it not well suited for a general exception handling mechanism. – Amit kumar Jun 09 '20 at 16:27
  • Ok, Can you show me how to make custom response for Spring Security, please? – Peter Penzov Jun 09 '20 at 16:29
  • Please visit : https://www.baeldung.com/exception-handling-for-rest-with-spring . It has a neat explanation, about different ways via which we can write custom response, along with implementation. Hope it helps! – Amit kumar Jun 09 '20 at 17:07

4 Answers4

2

Simply create a DTO class, and return as a normal response:

class Error {
    long status;
    long code;
    String title;
    String detail;
    Extra extra;

    // constructor for all argument...
}

Inside handler create an object of the above class and send the object in ResponseEntity:

Error error = new Error(args....);
return new ResponseEntity<Object>(error, HttpStatus.FORBIDDEN);
Emma
  • 27,428
  • 11
  • 44
  • 69
swarup bhol
  • 109
  • 1
  • 1
2

well, I think you can do custom error for access denied exception in this way

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;

@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    private final ObjectMapper om;

    public SecurityConfiguration() {
        this.om = new ObjectMapper();
        this.om.setDefaultPropertyInclusion(JsonInclude.Include.NON_NULL);
    }

    @Override
    protected void configure(final HttpSecurity http) throws Exception {
        http
            .exceptionHandling()
            .accessDeniedHandler((request, response, accessDeniedException) -> {
                response.setStatus(HttpServletResponse.SC_FORBIDDEN);
                response.setContentType(MediaType.APPLICATION_JSON_VALUE);
                ServletOutputStream out = response.getOutputStream();
                om.writeValue(
                    out,
                    ErrorDto
                    .builder()
                        .code(1000)
                        .title("Forbidden")
                        .detail("Forbidden")
                        .status(404)
                    .build()
                );
                out.flush();
            }).and()
            .authorizeRequests().antMatchers("/api/**").authenticated()
            .and()
            .httpBasic()
            .and()
            .formLogin().disable();
    }

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    @Builder
    private static class ErrorDto{
        private int status;
        private int code;
        private String title;
        private String detail;
        // other fields
    }
}

Boris Chistov
  • 940
  • 1
  • 9
  • 16
  • Thanks, one additional question. I get some additional params. Do you know why and how I can remove them? See here: https://stackoverflow.com/questions/62311702/remove-values-from-access-handler – Peter Penzov Jun 10 '20 at 20:56
  • check application settings for property spring.jackson.default-property-inclusion: non_null – Boris Chistov Jun 10 '20 at 21:14
  • there are several options for this setting: ALWAYS, NON_NULL, NON_ABSENT, NON_EMPTY, NON_DEFAULT, CUSTOM, USE_DEFAULTS, if you want separate settings for object mapper, then it's better not to inject it, but create new one and configure it for concrete case – Boris Chistov Jun 10 '20 at 21:17
  • Can you show me how to use separate settings for object mapper? – Peter Penzov Jun 10 '20 at 21:41
  • ye, sure. Check my answer. Just updated it. Look at the constructor, where I create new ObjectMapper – Boris Chistov Jun 10 '20 at 22:09
  • Thank you very much! – Peter Penzov Jun 10 '20 at 22:23
1

To implement custom exception handler:

  1. Specify the endpoint adress, which handles all error types and Exceptions, in the web.xml:
    <error-page>
        <location>/error</location>
    </error-page>
    
  2. Add corresponding error handler method:

    @RequestMapping(value = "/error", produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<?> handleError(
            @RequestAttribute(name = "javax.servlet.error.status_code",
                    required = false) Integer errorCode,
            @RequestAttribute(name = "javax.servlet.error.exception",
                    required = false) Throwable exception) {
    
        if (errorCode == null) {
            errorCode = 500;
        }
    
        String reasonPhrase;
        if (exception != null) {
            Throwable cause = exception.getCause();
            if (cause != null) {
                reasonPhrase = cause.getMessage();
            } else {
                reasonPhrase = exception.getMessage();
            }
        } else {
            reasonPhrase = HttpStatus.valueOf(errorCode).getReasonPhrase();
        }
    
        HashMap<String, String> serverResponse = new HashMap<>();
        serverResponse.put("errorCode", String.valueOf(errorCode));
        serverResponse.put("reasonPhrase", reasonPhrase);
    
        return ResponseEntity.status(errorCode).body(serverResponse);
    }
    
  3. Sending an ajax request from the frontend (I wrote it in AngularJS):

    http({
        url: '/someEndpoint',
        method: "POST",
        headers: {'Content-Type': undefined },
    }).then((response) => successCallback(response),
        (response) => errorCallback(response));
    
    let successCallback = function(response) {
        // console.log(response);
        mdToast.show({
            position: 'bottom right',
            template: scope.templates.success('Success message'),
        });
    };
    
    let errorCallback = function(response) {
        // console.log(response);
        mdToast.show({
            position: 'bottom right',
            template: scope.templates.error('Error message'),
        });
        if (response && response.data && response.data.reasonPhrase) {
            mdToast.show({
                position: 'bottom right',
                template: scope.templates.error(response.data.reasonPhrase),
            });
        }
    };
    

    First it says, that there was an error, and then says what error it was.


For example, if the server returns something unexpected:

@GetMapping({"/", "/index.html"})
public ResponseEntity<?> getMainPage() throws Exception {
    throw new Exception("Something unexpected..");
}

The client receives this kind of JSON message:

{
    "reasonPhrase": "Something unexpected..",
    "errorCode": "500"
}

Or server may return something less unexpected:

@GetMapping({"/", "/index.html"})
public ResponseEntity<?> getMainPage() {
    try {
        // expected actions
        // . . .
    } catch (Exception e) {
        HashMap<String, String> serverResponse = new HashMap<>();
        serverResponse.put("errorCode", "403"));
        serverResponse.put("reasonPhrase", "Forbidden");

        return ResponseEntity.status(403).body(serverResponse);
    }
}

Client:

{
    "reasonPhrase": "Forbidden",
    "errorCode": "403"
}

Also, if a client requests an unreachable resource or a page that does not exist, then he receives:

{
    "reasonPhrase": "Not Found",
    "errorCode": "404"
}
0

Return a String after using writeValueAsString:

return new ResponseEntity<String>(
        new ObjectMapper().writer().writeValueAsString(errorObj),
        HttpStatus.FORBIDDEN);
Ori Marko
  • 56,308
  • 23
  • 131
  • 233