Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Double error response handling on suspended methods #31541

Closed
nilskohrs opened this issue Aug 8, 2023 · 11 comments
Closed

Double error response handling on suspended methods #31541

nilskohrs opened this issue Aug 8, 2023 · 11 comments
Assignees
Labels
in: web Issues in web modules (web, webmvc, webflux, websocket) status: backported An issue that has been backported to maintenance branches type: bug A general bug
Milestone

Comments

@nilskohrs
Copy link

nilskohrs commented Aug 8, 2023

The combination of spring-boot-starter-web, kotlinx-coroutines-reactor, a suspended controller method and handling a 500 error response through ResponseEntityExceptionHandler causes double exception handling and response bodies.

The first response body is generated by the ProblemDetail handled with ResponseEntityExceptionHandler.handleException(...), the second is generated by BasicErrorController

Reproduction steps (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 code
@SpringBootApplication
class DemoApplication
fun main(args: Array<String>) {
    runApplication<DemoApplication>(*args)
}

@RestController
class Controller {
    @GetMapping("normal")
    fun normalScenario() {
        throw IllegalStateException("Returns problemdetail")
    }
    @GetMapping("suspend")
    suspend fun errorScenario() {
        throw IllegalStateException("Double exception handling")
    }
}

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

Endpoint /normal returns the expected Problem Detail

Endpoint /suspend with postman returns double body response (body size + ProblemDetail and then body size + BasicErrorController's response). Since this response is an invalid http response, a proxy like fiddler is needed to see this double response being returned.

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"}

Endpoint /suspend with chrome triggers following error:
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.

@nilskohrs nilskohrs changed the title Double error response handling on suspened methods Double error response handling on suspended methods Aug 8, 2023
@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged or decided on label Aug 8, 2023
@snicoll snicoll transferred this issue from spring-projects/spring-boot Aug 8, 2023
@snicoll snicoll transferred this issue from spring-projects/spring-framework Aug 8, 2023
@nilskohrs
Copy link
Author

nilskohrs commented Aug 10, 2023

I've found out that it's being triggered by setting the attribute jakarta.servlet.error.exception on the request. This happens in in ResponseEntityExceptionHandler.handleExceptionInternal(...)

...
if (statusCode.equals(HttpStatus.INTERNAL_SERVER_ERROR)) {
	request.setAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE, ex, WebRequest.SCOPE_REQUEST);
}
...

So I'm for now using a workaround by removing that attribute after calling ResponseEntityExceptionHandler.handleException(...)

@ControllerAdvice
class ExceptionHandler : ResponseEntityExceptionHandler() {
    @ExceptionHandler(Exception::class)
    fun handleUnexpectedTechnicalException(
        ex: Exception,
        request: WebRequest
    ): ResponseEntity<Any>? {
        return handleException(
            ErrorResponseException(
                HttpStatus.INTERNAL_SERVER_ERROR,
                ProblemDetail.forStatus(500),
                ex
            ), request
        ).also {
            request.removeAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE, WebRequest.SCOPE_REQUEST)
        }
    }
}

@nilskohrs
Copy link
Author

org.apache.catalina.connector.CoyoteAdapter does check the request attribute jakarta.servlet.error.exception to start the second error handling in asyncDispatch(...)

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

@nilskohrs
Copy link
Author

nilskohrs commented Aug 10, 2023

Just tested it with Jetty and Undertow. Both handle the exception correctly

@wilkinsona
Copy link
Member

wilkinsona commented Aug 10, 2023

Thanks for tracking that down, @nilskohrs. As the problem occurs with Tomcat but not with Jetty and Undertow, it would appear to be an issue with how Tomcat performs error handling for async requests (/cc @markt-asf). Please turn the steps and code snippets above into a complete yet minimal example and then open a Tomcat issue so that they can investigate.

@wilkinsona wilkinsona closed this as not planned Won't fix, can't repro, duplicate, stale Aug 10, 2023
@wilkinsona wilkinsona added status: invalid An issue that we don't feel is valid for: external-project Needs a fix in external project and removed status: waiting-for-triage An issue we've not yet triaged or decided on labels Aug 10, 2023
@nilskohrs
Copy link
Author

Bug reported with Tomcat: https://bz.apache.org/bugzilla/show_bug.cgi?id=66875

@markt-asf
Copy link

markt-asf commented Nov 2, 2023

@wilkinsona you might want to review the Tomcat bug report. There was a Tomcat issue that has been fixed but there might be a Spring issue as well. The AsyncContext isn't being completed/dispatched in the AsyncListener's onError() event. It depends on what is considered a framework responsibility and what is an application responsibility

@wilkinsona
Copy link
Member

wilkinsona commented Nov 2, 2023

Thanks, @markt-asf. I'll re-open this and we can transfer it to the Framework team so that they can review the behavior of org.springframework.web.context.request.async.StandardServletAsyncWebRequest (I assume that's relevant AsyncListener implementation here).

@wilkinsona wilkinsona reopened this Nov 2, 2023
@wilkinsona wilkinsona removed status: invalid An issue that we don't feel is valid for: external-project Needs a fix in external project labels Nov 2, 2023
@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged or decided on label Nov 2, 2023
@snicoll snicoll transferred this issue from spring-projects/spring-boot Nov 2, 2023
@jhoeller jhoeller added in: web Issues in web modules (web, webmvc, webflux, websocket) type: bug A general bug and removed status: waiting-for-triage An issue we've not yet triaged or decided on labels Dec 27, 2023
@jhoeller jhoeller added this to the 6.1.3 milestone Dec 27, 2023
@jhoeller
Copy link
Contributor

jhoeller commented Dec 27, 2023

@rstoyanchev according to https://bz.apache.org/bugzilla/show_bug.cgi?id=66875#c11, our StandardServletAsyncWebRequest.onError implementation should call asyncContext.complete() if that context is still non-null. Let's sort this out for 6.1.3 and backport it to 6.0.16 and 5.3.32 from there.

@injae-kim
Copy link
Contributor

injae-kim commented Jan 5, 2024

Hi everyone! based on above #31541 (comment), I create simple fix PR #31953!

PTAL when you have some times 🙇 thank you!

injae-kim added a commit to injae-kim/spring-framework that referenced this issue Jan 5, 2024
@rstoyanchev
Copy link
Contributor

rstoyanchev commented Jan 9, 2024

StandardServletAsyncWebRequest is only an adapter that delegates Servlet callbacks to DeferredResult, DeferredResultProcessingInterceptor, WebAsyncTask, CallableProcessingInterceptor, and the like. It does not know if the request should complete immediately. In this case, onError is after we've fully handled a controller exception through an ASYNC dispatch, but it may also come from the Servlet container unrelated to the controller in which case we would still want follow-up handling even if the response may no longer be usable. In short, I don't think we should complete the AsyncContext from StandardServletAsyncWebRequest.

From Mark's comment on the Tomcat issue:

  1. When Spring sets jakarta.servlet.error.exception that triggers Tomcat's internal error handling. Whether Spring should do that and whether that should have the effect it has are the first issue.

The request attribute was added a long time ago in 48b963a to assist with Servlet container defined error pages, and when it's only the response status that is set, it allows Boot's error handling to add a response body. However, with RFC 7807 responses we now set a response body, and shouldn't be setting the attribute if that's the case.

I verified that the fix on Tomcat's side apache/tomcat@276e04f does preclude the additional ERROR dispatch in this scenario, but we can also make a change to not even set the attribute when there is a response body.

@injae-kim
Copy link
Contributor

injae-kim commented Jan 9, 2024

Aha~ thank you @rstoyanchev for kind and detailed explanation! 👍

Anyway I'm big fan of spring-framework and want to contribute continuously so I'll try to resolve another issue~!

@rstoyanchev rstoyanchev added the for: backport-to-6.0.x Marks an issue as a candidate for backport to 6.0.x label Jan 9, 2024
@github-actions github-actions bot added status: backported An issue that has been backported to maintenance branches and removed for: backport-to-6.0.x Marks an issue as a candidate for backport to 6.0.x labels Jan 9, 2024
rstoyanchev added a commit that referenced this issue Jan 9, 2024
ResponseEntityExceptionHandler should not set the exception attribute
when there is a response body, and the response is fully handled.

Closes gh-31541
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: web Issues in web modules (web, webmvc, webflux, websocket) status: backported An issue that has been backported to maintenance branches type: bug A general bug
Projects
None yet
Development

Successfully merging a pull request may close this issue.

7 participants