https://bz.apache.org/bugzilla/show_bug.cgi?id=66875

            Bug ID: 66875
           Summary: Handling async error after spring already handled
                    error
           Product: Tomcat 10
           Version: 10.1.11
          Hardware: All
                OS: All
            Status: NEW
          Severity: normal
          Priority: P2
         Component: Catalina
          Assignee: dev@tomcat.apache.org
          Reporter: nils.ko...@gmail.com
  Target Milestone: ------

First reported as bug on Spring
Boot(https://github.com/spring-projects/spring-boot/issues/36803). After some
investigation I've been redirected by the spring team to post the bug here.

Reproduction steps with spring boot(tested on version 3.0.0 and 3.1.2):

- Create spring initializr project with Kotlin and spring-boot-starter-web
- Add org.jetbrains.kotlinx:kotlinx-coroutines-reactor dependency
- Run following (Kotlin) code

---
@SpringBootApplication
class DemoApplication
fun main(args: Array<String>) {
    runApplication<DemoApplication>(*args)
}
@RestController
class Controller {
    @GetMapping("suspend")
    suspend fun errorScenario() {
        throw IllegalStateException("Double exception handling")
    }
}

@ControllerAdvice
class ExceptionHandler : ResponseEntityExceptionHandler() {
    @ExceptionHandler(Exception::class)
    fun handleUnexpectedException(
        ex: Exception,
        request: WebRequest
    ): ResponseEntity<Any>? {
        return handleException(
            ErrorResponseException(
                HttpStatus.INTERNAL_SERVER_ERROR,
                ProblemDetail.forStatus(500),
                ex
            ), request
        )
    }
}
---

When calling the /suspend endpoint (with postman) spring will catch the
exception and create a response. If that response is has status 500 it will
also set "jakarta.servlet.error.exception" with the caught exception.
Because of this, this code in
org.apache.catalina.connector.CoyoteAdapter.asyncDispatch(...) will trigger a
second exception handling.

---
if (request.isAsyncDispatching()) {
   
connector.getService().getContainer().getPipeline().getFirst().invoke(request,
response);
    Throwable t = (Throwable)
request.getAttribute(RequestDispatcher.ERROR_EXCEPTION);
    if (t != null) {
        asyncConImpl.setErrorState(t, true);
    }
}
---

The result of this is that two response bodies, including body size, will be
generated and returned. Postman will show that the request had a problem. This
is because there is more data than the first body size reports, which is an
invalid http response. Sniffing the response you can actually see the full
response:

---
HTTP/1.1 500
Content-Type: application/problem+json
Transfer-Encoding: chunked
Date: Tue, 08 Aug 2023 09:26:30 GMT
Connection: close

59
{"type":"about:blank","title":"Internal Server
Error","status":500,"instance":"/suspend"}
6c
{"timestamp":"2023-08-08T09:26:30.705+00:00","status":500,"error":"Internal
Server Error","path":"/suspend"}
---

When calling the endpoint with chrome it will generate following error message,
but only return a single response:
s.e.ErrorMvcAutoConfiguration$StaticView : Cannot render error page for request
[/suspend] as the response has already been committed. As a result, the
response may have the wrong status code.

This bug goes for me far too deep into the inner workings and I don't know if
this is a tomcat bug or spring should just not set the the request attribute
"jakarta.servlet.error.exception". It is however notable that when using
another server(jetty, undertow) underneath spring this issue does not occur.
And that is probably why the spring team send me here.

-- 
You are receiving this mail because:
You are the assignee for the bug.
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org
For additional commands, e-mail: dev-h...@tomcat.apache.org

Reply via email to