Use StringDecoder to split SSE stream

ServerSentEventHttpMessageReader had logic to split on new lines
and buffer until an empty new line (start of a new event). To account
for random data chunking, it later re-assembled the lines for each
event and split again on new lines. However bufferUntil was still
unreliable a chunk may contain nothing but a newline, which doesn't
necessarily mean an empty newline in the overall SSE stream.

This commit simplifies the above by delegating the splitting of the
stream along newlines to StringDecoder.

Issue: SPR-16744
This commit is contained in:
Rossen Stoyanchev
2018-04-19 11:29:12 -04:00
parent 30c98c8a1c
commit da98ff72d2
2 changed files with 62 additions and 95 deletions

View File

@@ -18,6 +18,7 @@ package org.springframework.web.reactive.result.method.annotation;
import java.time.Duration;
import org.junit.Assume;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
@@ -32,8 +33,10 @@ import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.codec.ServerSentEvent;
import org.springframework.http.server.reactive.AbstractHttpHandlerIntegrationTests;
import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.http.server.reactive.bootstrap.JettyHttpServer;
import org.springframework.http.server.reactive.bootstrap.ReactorHttpServer;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.reactive.DispatcherHandler;
import org.springframework.web.reactive.config.EnableWebFlux;
@@ -103,51 +106,42 @@ public class SseIntegrationTests extends AbstractHttpHandlerIntegrationTests {
@Test
public void sseAsEvent() {
Flux<ServerSentEvent<String>> result = this.webClient.get()
Assume.assumeTrue(server instanceof JettyHttpServer);
Flux<ServerSentEvent<Person>> result = this.webClient.get()
.uri("/event")
.accept(TEXT_EVENT_STREAM)
.retrieve()
.bodyToFlux(new ParameterizedTypeReference<ServerSentEvent<String>>() {});
.bodyToFlux(new ParameterizedTypeReference<ServerSentEvent<Person>>() {});
StepVerifier.create(result)
.consumeNextWith( event -> {
assertEquals("0", event.id());
assertEquals("foo", event.data());
assertEquals("bar", event.comment());
assertNull(event.event());
assertNull(event.retry());
})
.consumeNextWith( event -> {
assertEquals("1", event.id());
assertEquals("foo", event.data());
assertEquals("bar", event.comment());
assertNull(event.event());
assertNull(event.retry());
})
.thenCancel()
.verify(Duration.ofSeconds(5L));
verifyPersonEvents(result);
}
@Test
public void sseAsEventWithoutAcceptHeader() {
Flux<ServerSentEvent<String>> result = this.webClient.get()
Flux<ServerSentEvent<Person>> result = this.webClient.get()
.uri("/event")
.accept(TEXT_EVENT_STREAM)
.retrieve()
.bodyToFlux(new ParameterizedTypeReference<ServerSentEvent<String>>() {});
.bodyToFlux(new ParameterizedTypeReference<ServerSentEvent<Person>>() {});
verifyPersonEvents(result);
}
private void verifyPersonEvents(Flux<ServerSentEvent<Person>> result) {
StepVerifier.create(result)
.consumeNextWith( event -> {
assertEquals("0", event.id());
assertEquals("foo", event.data());
assertEquals("bar", event.comment());
assertEquals(new Person("foo 0"), event.data());
assertEquals("bar 0", event.comment());
assertNull(event.event());
assertNull(event.retry());
})
.consumeNextWith( event -> {
assertEquals("1", event.id());
assertEquals("foo", event.data());
assertEquals("bar", event.comment());
assertEquals(new Person("foo 1"), event.data());
assertEquals("bar 1", event.comment());
assertNull(event.event());
assertNull(event.retry());
})
@@ -180,6 +174,7 @@ public class SseIntegrationTests extends AbstractHttpHandlerIntegrationTests {
@RestController
@SuppressWarnings("unused")
@RequestMapping("/sse")
static class SseController {
private static final Flux<Long> INTERVAL = interval(Duration.ofMillis(100), 50);
@@ -187,25 +182,26 @@ public class SseIntegrationTests extends AbstractHttpHandlerIntegrationTests {
private MonoProcessor<Void> cancellation = MonoProcessor.create();
@GetMapping("/sse/string")
@GetMapping("/string")
Flux<String> string() {
return INTERVAL.map(l -> "foo " + l);
}
@GetMapping("/sse/person")
@GetMapping("/person")
Flux<Person> person() {
return INTERVAL.map(l -> new Person("foo " + l));
}
@GetMapping("/sse/event")
Flux<ServerSentEvent<String>> sse() {
return INTERVAL.map(l -> ServerSentEvent.builder("foo")
.id(Long.toString(l))
.comment("bar")
.build());
@GetMapping("/event")
Flux<ServerSentEvent<Person>> sse() {
return INTERVAL.take(2).map(l ->
ServerSentEvent.builder(new Person("foo " + l))
.id(Long.toString(l))
.comment("bar " + l)
.build());
}
@GetMapping("/sse/infinite")
@GetMapping("/infinite")
Flux<String> infinite() {
return Flux.just(0, 1).map(l -> "foo " + l)
.mergeWith(Flux.never())