Spring REST @ResponseStatus with Custom exception class does not change the return Status code
Asked Answered
S

3

7

I have a exception class like follows

@ResponseStatus(value=HttpStatus.UNPROCESSABLE_ENTITY, reason="Unprocessable Entity")  // 422
public class UnprocessableEntityException extends RuntimeException {
}

Now the status is not returned as 422 unless I write a specific handler in the Controller class like :

@ExceptionHandler(UnprocessableEntityException.class)
    @ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY)
    public String handleException(Exception ex) {
...
}

As I understand I should not need @ExceptionHandler in first place, not sure what am I missing.

Spahi answered 3/12, 2015 at 19:45 Comment(0)
H
3

Throwing a @ResponseStatus annotated exception from a controller method should be enough for the framework to write the HTTP status code - no @ExceptionHandler necessary.

The following will write a 422 Status on hitting the webapp root as expected:

@Controller
public class ExceptionController {

    @RequestMapping("/")
    public void action() {
        throw new ActionException();
    }

    @ResponseStatus(value = HttpStatus.UNPROCESSABLE_ENTITY, reason = "nope")
    public static class ActionException extends RuntimeException {}
}

This works courtesy of the ResponseStatusExceptionResolver which is created by Spring MVC by default - if it's not working for you, my guess is that this default exception resolver has been removed (by e.g. overriding WebMvcConfigurationSupport.configureHandlerExceptionResolvers or otherwise configuring your context's HandlerExceptionResolvers so that the ResponseStatusExceptionResolver is trumped.)

Hearsh answered 3/12, 2015 at 22:14 Comment(4)
Annotating your custom Exception class with @ResponseStatus smells like poor design to me. In general, your Exception classes should not have platform-specific dependencies. In my organization we use 422 for business logic errors (e.g., schedule time off in a past date). We throw a custom unchecked exception for those. If you're in a Kafka consumer and detect one such error you may throw that exception. But if it's annotated with @ResponseStatus your Kafka consumer program requires a dependency to spring web. :^(Hackathorn
I definitely wouldn't advocate throwing exceptions from deep in your business logic with a spring-web dependency, but instead you would translate such exceptions to web-tier specific ones from within your controllers or elsewhere in your web tier.Hearsh
Why translate any exception coming from application or domain layer into a REST adapter-specific exception that you created?Hackathorn
Personally I just think this is good design: you probably already convert your domain entities into protocol-specific DTOs. Why not do the same for exceptions? You just need a single annotated exception class with your 422 status, and a tiny amount of code to do the conversion - with a bit of AOP the work to do this everywhere would fall away to almost nothing. Maybe your situation wouldn't get any value out of this approach and you would be better off implementing your own HandlerExceptionResolver for example, but I don't think it's necessarily poor design.Hearsh
R
1

The exception thrown should not be handled by code or by other exception resolvers, for example it shouldn't be handled by @ExceptionHandler, because that will override the status code specified by the exception class's @ResponseStatus.

Romanism answered 6/11, 2017 at 5:50 Comment(0)
U
0

Annotating the exception class with ResponseStatus which you use from the service layer, is not good practice.

The best solution is to create your own exception classes by extending Exception or RuntimeException. Then create a global exception handler using the ControllerAdvice annotation, where you can easily set the HTTP response status code per exception.

If you want to rollback the JPA transaction, extend the RuntimeException because the Spring @Repository does DB rollback in this case by default.

Example custom exception classes:

public class AlreadyRegisteredException extends RuntimeException {

    public AlreadyRegisteredException(final String message) {
        super(message);
    }
}

public class EntityNotFoundException extends RuntimeException {

    public EntityNotFoundException(final String message) {
        super(message);
    }
}

The global exception handler class:

@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    @ExceptionHandler(AlreadyRegisteredException.class)
    public ResponseEntity<String> handleAlreadyRegisteredException(AlreadyRegisteredException ex) {
        log.error(ex);
        return new ResponseEntity<>(ex.getMessage(), HttpStatus.ALREADY_REPORTED);
    }

    @ExceptionHandler(EntityNotFoundException.class)
    public ResponseEntity<String> handleEntityNotFoundException(EntityNotFoundException ex) {
        var reason = ex.getMessage();
        log.warn(reason);
        return new ResponseEntity<>(reason, HttpStatus.NOT_FOUND);
    }
}

USAGE:

Rest endpoint

@Slf4j
@RestController
@RequestMapping("/customer")
@RequiredArgsConstructor
public class CustomerController {

    private final CustomerService customerService;

    @PostMapping("/register")
    public void registerByEmail(@RequestBody Customer customer) {
        customerService.register(customer);
    }
}

Service layer

@Component
@Slf4j
@RequiredArgsConstructor
public class CustomerService {

    private final CustomerRepository customerRepository;

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void register(Customer customer) {
        Optional<String> email = getEmail(customer);
        if (email.isEmpty()) {
            throw new EmptyIdException("Unable to register a new customer. Customer's email is null.");
        }

        Optional<CustomerEntity> existingCustomer = customerRepository.findByEmail(Id.get());
        if (existingCustomer.isPresent()){
            throw new AlreadyRegisteredException(String.format("Unable to register a new customer. Customer with the "
                    + "same email has already been registered: {email: \"%s\"}", email.get()));
        } else {
            customerRepository.save(...);
        }
    }
}

I hope that it helps.

Undercroft answered 14/8 at 16:3 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.