Affects: Spring Framework 6.1.3 with Tomcat >= 10.1.16 (or Spring Boot >= 3.1.6, including 3.2.x)

Description

When an IOException is thrown during resolving method argument values for a Rest Controller (in Tomcat's InputBuffer#realReadBytes#313, invoked by Spring's AbstractMessageConverterMethodArgumentResolver#readWithMessageConverters#174), Spring's DefaultHandlerExceptionResolver#handleHttpMessageNotReadable:587 throws an IllegalStateException with message Cannot call sendError() after the response has been committed.

This also results in setting HTTP response status code to 500 instead of 400.

In Spring Framework 6.1.3 with Tomcat 10.1.15 the IllegalStateException is not thrown, and the HTTP status code is 400.

Reproduction

See https://github.com/mgocd/spring-httpmessagenotreadableexception-resolve-issue

Analysis

Starting from Tomcat 10.1.16 (because of this commit) the exception handler inside InputBuffer#realReadBytes runs response.sendError(400), making the httpServletResponse.isCommitted() return true starting from this point.

When Spring's DefaultHandlerExceptionResolver#handleHttpMessageNotReadable:587 runs response.sendError(400) again, it throws the IllegalStateException: Cannot call sendError() after the response has been committed.

To resolve the issue, DefaultHandlerExceptionResolver#handleHttpMessageNotReadable could run response.sendError conditionally, only if !response.isCommitted(), similarly as done in DefaultHandlerExceptionResolver#handleErrorResponse:514.