It seems like there is a regression introduced into SseEmitter
in latest 5.2.10.RELEASE
(apparently https://github.com/spring-projects/spring-framework/issues/25442), it now returns to the client only first SSE event.
How to reproduce
package com.example.sse.emmiter;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter.SseEventBuilder;
@SpringBootApplication
public class SseEmitterRegressionApplication {
@RestController
@EnableAutoConfiguration
static class LibraryController {
@GetMapping("/sse")
public SseEmitter streamSseMvc() {
final SseEmitter emitter = new SseEmitter();
final ExecutorService sseMvcExecutor = Executors.newSingleThreadExecutor();
sseMvcExecutor.execute(() -> {
try {
for (int eventId = 1; eventId <= 5; ++eventId) {
SseEventBuilder event = SseEmitter.event()
.id(Integer.toString(eventId))
.data(new Book("New Book #" + eventId, "Author #" + eventId), MediaType.APPLICATION_JSON)
.name("book");
emitter.send(event);
Thread.sleep(100);
}
emitter.complete();
} catch (Exception ex) {
emitter.completeWithError(ex);
}
});
return emitter;
}
}
static class Book {
private String title;
private String author;
public Book() {
}
public Book(final String title, final String author) {
this.setTitle(title);
this.setAuthor(author);
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
@Override
public int hashCode() {
return HashCodeBuilder.reflectionHashCode(this);
}
@Override
public boolean equals(Object obj) {
return EqualsBuilder.reflectionEquals(this, obj);
}
@Override
public String toString() {
return ToStringBuilder.reflectionToString(this);
}
}
public static void main(String[] args) {
SpringApplication.run(SseEmitterRegressionApplication.class, args);
}
}
- using latest Spring Boot
2.3.4.RELEASE
and Spring Framework5.2.9.RELEASE
$ curl http://localhost:8080/sse -iv
> GET /sse HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.71.1
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200
< Content-Type: text/event-stream
< Transfer-Encoding: chunked
< Date: Wed, 28 Oct 2020 22:09:00 GMT
<
{ [15 bytes data]
100 335 0 335 0 0 589 0 --:--:-- --:--:-- --:--:-- 590HTTP/1.1 200
Content-Type: text/event-stream
Transfer-Encoding: chunked
Date: Wed, 28 Oct 2020 22:09:00 GMT
id:1
data:{"title":"New Book #1","author":"Author #1"}
event:book
id:2
data:{"title":"New Book #2","author":"Author #2"}
event:book
id:3
data:{"title":"New Book #3","author":"Author #3"}
event:book
id:4
data:{"title":"New Book #4","author":"Author #4"}
event:book
id:5
data:{"title":"New Book #5","author":"Author #5"}
event:book
- using latest Spring Boot
2.3.4.RELEASE
and Spring Framework5.2.10.RELEASE
(overriding with<spring-framework.version>5.2.10.RELEASE</spring-framework.version>
)
$ curl http://localhost:8080/sse -iv
> GET /sse HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.71.1
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200
< Content-Type: text/event-stream
< Transfer-Encoding: chunked
< Date: Wed, 28 Oct 2020 22:10:33 GMT
<
{ [15 bytes data]
100 54 0 54 0 0 2250 0 --:--:-- --:--:-- --:--:-- 2250HTTP/1.1 200
Content-Type: text/event-stream
Transfer-Encoding: chunked
Date: Wed, 28 Oct 2020 22:10:33 GMT
id:1
data:{"title":"New Book #1","author":"Author #1"}
Reproducible all the time. Please advice if this is a regression or SseEmitter
semantics has changed (would appreciate documentation pointers) or more details are needed, thank you.
Comment From: rstoyanchev
The regression is due to optimizations in Jackson codecs and converters, issue #25910.
Comment From: reta
Thanks a lot for quick update, @rstoyanchev
Comment From: christophejan
It's look that there is the same regression with controller producing multipart. The server response is truncated after any jackson part body (the boundary after this part is missing, all remaining parts are ignored).
Comment From: rstoyanchev
Yes this will impact all cases in Spring MVC where Jackson is used to write but the response needs to remain open.
Comment From: rstoyanchev
This should be fixed now for 5.2.11 and 5.3.1.
In the mean time as a workaround you could disable the JsonGenerator.Feature.AUTO_CLOSE_TARGET
on the ObjectMapper. For example in Spring Boot:
@Bean
public Jackson2ObjectMapperBuilderCustomizer om() {
return builder -> builder.featuresToDisable(JsonGenerator.Feature.AUTO_CLOSE_TARGET);
}