explorer
explorer

Reputation: 516

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

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.

Upvotes: 7

Views: 10585

Answers (3)

zappee
zappee

Reputation: 22646

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.

Upvotes: 0

artkoshelev
artkoshelev

Reputation: 892

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.

Upvotes: 1

ryanp
ryanp

Reputation: 5127

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.)

Upvotes: 3

Related Questions