spring-projects / spring-framework

Spring Framework
https://spring.io/projects/spring-framework
Apache License 2.0
56.32k stars 38.01k forks source link

Sever-Sent Events (SseEmitter) works unexpectedly on Undertow #27252

Closed waitshang closed 3 years ago

waitshang commented 3 years ago

Sever-Sent Events (SseEmitter) works unexpectedly on Undertow server (spring-boot-starter-undertow). The method SseEmitter.onError() cann't call back when client disconnected from server. This phenomenon does not happen when using Tomcat server.

Code

package com.shang.sse.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * Created by shangwei2009@hotmail.com on 2021/8/5 10:50
 */
@RestController
@Slf4j
public class SseController {

    private static final ExecutorService pool = Executors.newSingleThreadExecutor();

    @GetMapping("/sse")
    public SseEmitter sse() {
        final SseEmitter emitter = new SseEmitter();

        emitter.onError(throwable -> {
            log.error("onError");
        });
        emitter.onCompletion(() -> {
            log.info("onCompletion");
        });

        pool.execute(() -> {
            try {
                for (int i = 0; i < 3; i++) {
                    TimeUnit.SECONDS.sleep(1);
                    emitter.send(i);
                }
                emitter.complete();
            } catch (Exception ex) {
                emitter.completeWithError(ex);
            }
        });

        return emitter;
    }
}

Result

When disconnected from client, the onError and onCompletion message both not printed immediately.

result

After a while, only the onCompletion message printed.

after

Attachments

undertow-sse-demo.zip

wilkinsona commented 3 years ago

Thanks for the report and the sample. Please note that Spring Boot 2.3.x has reached the end of its open-source support period. I updated the sample to 2.4.9 and the problem still occurs. The analysis that follows was performed on Spring Boot 2.4.9 (Undertow 2.2.9 and Tomcat 9.0.50).

When emitter.send(i) is called after the client has disconnected, it fails with an IOException due to a broken pipe. This causes SseEmitter to set its sendFailed flag to true. The purpose of this flag is described by its javadoc:

After an I/O error, we don't call completeWithError directly but wait for the Servlet container to call us via AsyncListener#onError on a container thread at which point we call completeWithError. This flag is used to ignore further calls to complete or completeWithError that may come for example from an application try-catch block on the thread of the I/O error.

When you're using Undertow, the expected call to AsyncListener#onError never comes and, instead, the async request eventually times out. If you use Tomcat, the onError call does occur and your onError handler is called as expected.

This looks like an Undertow bug to me but we'll transfer the issue to the Framework team just in case there's anything that they can do.

waitshang commented 3 years ago

Thanks for the report and the sample. Please note that Spring Boot 2.3.x has reached the end of its open-source support period. I updated the sample to 2.4.9 and the problem still occurs. The analysis that follows was performed on Spring Boot 2.4.9 (Undertow 2.2.9 and Tomcat 9.0.50).

When emitter.send(i) is called after the client has disconnected, it fails with an IOException due to a broken pipe. This causes SseEmitter to set its sendFailed flag to true. The purpose of this flag is described by its javadoc:

After an I/O error, we don't call completeWithError directly but wait for the Servlet container to call us via AsyncListener#onError on a container thread at which point we call completeWithError. This flag is used to ignore further calls to complete or completeWithError that may come for example from an application try-catch block on the thread of the I/O error.

When you're using Undertow, the expected call to AsyncListener#onError never comes and, instead, the async request eventually times out. If you use Tomcat, the onError call does occur and your onError handler is called as expected.

This looks like an Undertow bug to me but we'll transfer the issue to the Framework team just in case there's anything that they can do.

Thanks a lot. I will pay attention to this problem continually.