I am building a spring boot application to handle sever sent events (SSE):

package com.example.demo;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;

@Component
@Slf4j
public class SseBroadcaster {

    private final ConcurrentHashMap<String, SseEmitter> emitters = new ConcurrentHashMap<>();

    public void addEmitter(String sessionId, SseEmitter emitter) {
        emitters.put(sessionId, emitter);
    }

    public void removeEmitter(String sessionId) {
        emitters.remove(sessionId);
    }

    public void broadcast(String message) {
        for (String s : emitters.keySet()){
            try {
                emitters.get(s).send(SseEmitter.event().name("message").data(message));
                log.info("Message has been sent to " + s);
            } catch (IOException e) {
                log.error("Failed to send message to " + s, e);
                removeEmitter(s);
            }
        }
    }
}

package com.example.demo;

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

import java.util.UUID;

@RestController
@Slf4j
public class SseController {

    @Autowired
    private SseBroadcaster sseBroadcaster;

    @GetMapping("/api/contest/sse")
    public SseEmitter connectToSse() {
        String username = "(Unknown)";
        SseEmitter emitter = new SseEmitter(Long.MAX_VALUE);
        String sessionId = username + "-" + UUID.randomUUID().toString();
        sseBroadcaster.addEmitter(sessionId, emitter);
        log.info(username + " connected to SSE, session: " + sessionId);
        emitter.onCompletion(() -> {
            sseBroadcaster.removeEmitter(sessionId);
            log.info(username + " disconnected from SSE, session: " + sessionId);
        });
        emitter.onTimeout(() -> {
            sseBroadcaster.removeEmitter(sessionId);
            log.info(username + " disconnected from SSE, session: " + sessionId);
        });
        return emitter;
    }

    @PostMapping("/api/contest/admin/broadcast")
    public void publishBroadcast(String broadcast) {
        sseBroadcaster.broadcast(broadcast);
        log.info("Published a broadcast: " + broadcast);
    }

}

package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

}

As for frontend, use JavaScript to establish a SSE connection:

const reconnectInterval = 1000
let source = null
function createNewEventSource() {
    if (source) {
        source.close()
    }
    source = new EventSource('/api/contest/sse')
    source.onmessage = function(event) {
        alert(event.data)
    }
    source.onerror = function(event) {
        console.error('SSE connection error: ', event)
        setTimeout(createNewEventSource, reconnectInterval)
    }
}
createNewEventSource()

However, messages can not be sent instantly to the client. It was observed from logs that the client had successfully connected to the server URI /api/contest/sse , the server had received broadcast message from /api/contest/admin/broadcast and had sent the received message to client, but the client didn't receive that message.

2024-09-14T21:47:10.092+08:00  INFO 16020 --- [demo] [           main] com.example.demo.DemoApplication         : Starting DemoApplication using Java 21.0.4 with PID 16020 (C:\develop\demo\target\classes started by gengy in C:\develop\demo)
2024-09-14T21:47:10.095+08:00  INFO 16020 --- [demo] [           main] com.example.demo.DemoApplication         : No active profile set, falling back to 1 default profile: "default"
2024-09-14T21:47:10.683+08:00  INFO 16020 --- [demo] [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port 8080 (http)
2024-09-14T21:47:10.693+08:00  INFO 16020 --- [demo] [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2024-09-14T21:47:10.694+08:00  INFO 16020 --- [demo] [           main] o.apache.catalina.core.StandardEngine    : Starting Servlet engine: [Apache Tomcat/10.1.28]
2024-09-14T21:47:10.723+08:00  INFO 16020 --- [demo] [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2024-09-14T21:47:10.723+08:00  INFO 16020 --- [demo] [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 571 ms
2024-09-14T21:47:10.956+08:00  INFO 16020 --- [demo] [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port 8080 (http) with context path '/'
2024-09-14T21:47:10.962+08:00  INFO 16020 --- [demo] [           main] com.example.demo.DemoApplication         : Started DemoApplication in 1.083 seconds (process running for 1.418)
2024-09-14T21:47:12.667+08:00  INFO 16020 --- [demo] [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
2024-09-14T21:47:12.668+08:00  INFO 16020 --- [demo] [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
2024-09-14T21:47:12.668+08:00  INFO 16020 --- [demo] [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 0 ms
2024-09-14T21:47:12.688+08:00  INFO 16020 --- [demo] [nio-8080-exec-1] com.example.demo.SseController           : (Unknown) connected to SSE, session: (Unknown)-2e96934e-8a86-4267-bbbd-48270698a79b
2024-09-14T21:47:22.963+08:00  INFO 16020 --- [demo] [nio-8080-exec-2] com.example.demo.SseBroadcaster          : Message has been sent to (Unknown)-2e96934e-8a86-4267-bbbd-48270698a79b
2024-09-14T21:47:22.963+08:00  INFO 16020 --- [demo] [nio-8080-exec-2] com.example.demo.SseController           : Published a broadcast: 5678

If I terminated the spring boot application, client would received the message and then close the connection. Another several lines of logs are printed:

2024-09-14T21:47:34.371+08:00  INFO 16020 --- [demo] [nio-8080-exec-3] com.example.demo.SseController           : (Unknown) disconnected from SSE, session: (Unknown)-2e96934e-8a86-4267-bbbd-48270698a79b
2024-09-14T21:47:34.382+08:00  WARN 16020 --- [demo] [nio-8080-exec-3] .w.s.m.s.DefaultHandlerExceptionResolver : Ignoring exception, response committed already: org.springframework.web.context.request.async.AsyncRequestTimeoutException
2024-09-14T21:47:34.382+08:00  WARN 16020 --- [demo] [nio-8080-exec-3] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.context.request.async.AsyncRequestTimeoutException]
2024-09-14T21:47:34.382+08:00  INFO 16020 --- [demo] [nio-8080-exec-3] com.example.demo.SseController           : (Unknown) disconnected from SSE, session: (Unknown)-2e96934e-8a86-4267-bbbd-48270698a79b

In short, client will never receive the message until I close the backend spring boot application. It is suspected to be an issue with spring's SSE processing or detailed illustration should be provided on [SseEmitter (Spring Framework 6.1.13 API)](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/servlet/mvc/method/annotation/SseEmitter.html#send(java.lang.Object)). I would be appreciate it if this bug can be fixed in the next release.

Comment From: philwebb

SSE support is provided by Spring Framework, if you believe this is a bug please open an issue at https://github.com/spring-projects/spring-framework/issues/ along with a sample application that can be downloaded and run.