简体   繁体   中英

Spring MVC - @ExceptionHandler based on Accept header

I have a HandlerInterceptorAdapter that intercepts all requests and performs user authorization checks. Very basically:

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    User user = ... // get user
    checkIfAuthorized(user); // throws AuthorizationException
    return true;
}

I then have an @ExceptionHandler for that AuthorizationException .

@ExceptionHandler(value = AuthorizationException.class) 
public ResponseEntity<String> handleNotAuthorized(AuthorizationException e) {
    // TODO Custom EXCEPTION HANDLER for json/jsp/xml/other types, based on content type
    ResponseEntity<String> responseEntity = new ResponseEntity<>("You are not authorized to access that page.", HttpStatus.UNAUTHORIZED);
    return responseEntity;
}

This is fine if the (unauthorized) request accepts text/plain (and can be easily changed for json). How can I make different @ExceptionHandler s for specific Accept headers?

@RequestMapping has produces() . Is there something similar for @ExceptionHandler ?

I think of two approaches:

Manually

public ResponseEntity<String> handleNotAuthorized(AuthorizationException e, HttpServletRequest request) {
    // TODO Custom EXCEPTION HANDLER for json/jsp/xml/other types, based on content type
    if (/*read header accept from request and build appropiate response*/) {}
    ResponseEntity<String> responseEntity = new ResponseEntity<>("You are not authorized to access that page.", HttpStatus.UNAUTHORIZED);
    return responseEntity;

Automatically

@ResponseBody
public SomeObject handleNotAuthorized(AuthorizationException e, HttpServletRequest request) {
    // TODO Custom EXCEPTION HANDLER for json/jsp/xml/other types, based on content type
    /* Construct someObject and let Spring MessageConverters transform it to JSON or XML. I don't remember what happens in case of HTML (it should go to a view)*/
    return someObject;

Don't forget to set the Response's Status code.

I know this comes late but I've been looking up a solution to this, came across this question and found what I think to be a better solution. You can return "forward:/error" in your @ExceptionHandler (returning a String) to forward the request to a

@RequestMapping("/error")
ErrorController {...}

and use

@RequestMapping(produces = "text/html") 
ModelAndView errorPage() {...}

on one method of that ErrorController,

@RequestMapping(produces = "application/json") // or no 'produces' attribute for a default
MyJsonObject errorJson() {...} on another.

I think this is a pretty neat way to do it, it's probably already out there but I didn't find it when trying to look it up.

So basically the @ExceptionHandler is the same for all, but forwards to a controller that can do the usual stuff

Not exactly the same use case, but the same requirement. I solve it with a custom HttpMessageConverter implementation.

@RestController
@RequestMapping("/foo")
public class MyResource {

    @GetMapping(path = "/{id}", produces = "application/json")
    public ResponseEntity<MyDto> get (@PathVariable(ID) long id)
            throws IOException {

        throw new MyCustomException();
    }

    @GetMapping(path = "/{id}/export", produces = "application/zip")
    public ResponseEntity<byte[]> export (@PathVariable(ID) long id)
            throws IOException {

        throw new MyCustomException();
    }
}

...

@ControllerAdvice
public class MyCustomExceptionHandler {

    @ResponseBody
    @ExceptionHandler
    @ResponseStatus(BAD_REQUEST)
    public JsonAPIErrorDocument handleException (MyCustomException e) {

        return ....;
    }
}

...

public class JsonAPIErrorDocumentToByteArrayMessageConverter extends AbstractHttpMessageConverter {

    public ErrorDocumentToByteArrayMessageConverter () {

        super(new MediaType("application", "zip"), MediaType.ALL);
    }

    @Override
    protected boolean supports (Class clazz) {

        return JsonAPIErrorDocument.class == clazz;
    }

    @Override
    protected Object readInternal (Class clazz, HttpInputMessage inputMessage)
            throws IOException,
            HttpMessageNotReadableException {

        return new byte[0];
    }

    @Override
    protected void writeInternal (Object t, HttpOutputMessage outputMessage)
            throws IOException,
            HttpMessageNotWritableException {

    }
}

...

@EnableWebMvc
@Configuration
@ComponentScan({ "com.foo" })
public class ApplicationConfig implements WebMvcConfigurer {

    ...

    @Override
    public void configureMessageConverters (List<HttpMessageConverter<?>> converters) {

        converters.add(new MappingJackson2HttpMessageConverter(objectMapper));
        converters.add(new ByteArrayHttpMessageConverter());
        converters.add(new JsonAPIErrorDocumentToByteArrayMessageConverter());
    }

    ...
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM