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.