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