There also appears to be a bug/inconsistency in the Chain of Resolvers in that it states

null if the exception remains unresolved, for subsequent resolvers to try, and, if the exception remains at the end, it is allowed to bubble up to the Servlet container.

But returning null instead of error/500 gives me a 0-byte content. I was expecting null to forward up to the default handlers one of which is to render src/main/resources/error/500.html which occurs when I don't have an @ExceptionHandler but it didn't work so I basically had to check if it is an HTML request and route accordingly

@ControllerAdvice
public class GeneralExceptionHandler implements Ordered {

  @Override
  public int getOrder() {
    return Ordered.LOWEST_PRECEDENCE;
  }

  @ExceptionHandler(Throwable.class)
  public Object handleException(@NotNull Throwable e, @NotNull final WebRequest request) {

    final var acceptHeader = request.getHeader(HttpHeaders.ACCEPT);
    var isHtmlRequest = false;
    if (acceptHeader != null) {
      isHtmlRequest = Arrays.asList(acceptHeader.split(",")).contains(MediaType.TEXT_HTML_VALUE);
    }
    if (isHtmlRequest) {
      return "error/500"; // ideally a return `null` should work as expected here.
    } else {
      return ResponseEntity.internalServerError()
          .body(
              StatusProto.fromThrowable(
                  io.grpc.Status.INTERNAL
                      .withDescription(e.getMessage())
                      .withCause(e)
                      .asRuntimeException()));
    }
  }
}

Ref: https://stackoverflow.com/questions/77176531/how-do-i-allow-for-500-html-to-be-used-in-spring-boot-only-when-the-request-was/77182791#77182791

Comment From: bclozel

This is the expected behavior.

This documentation snippet is about the chain of HandlerExceptionResolver. The list of resolver is defined just above this section. The actual behavior @ExceptionHandler method is implemented by ExceptionHandlerExceptionResolver and explained in the dedicated section.