WebFlux support for SSE with multiline fragments

See gh-33194
This commit is contained in:
rstoyanchev
2024-08-06 11:06:21 +03:00
parent b734156f32
commit 8e2b27e5d8
9 changed files with 138 additions and 39 deletions

View File

@@ -41,6 +41,7 @@ import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.MediaType;
@@ -538,21 +539,25 @@ public class ViewResolutionResultHandler extends HandlerResultHandlerSupport imp
@Override
public Flux<DataBuffer> format(
Flux<DataBuffer> fragmentContent, Fragment fragment, ServerWebExchange exchange) {
Flux<DataBuffer> fragmentFlux, Fragment fragment, ServerWebExchange exchange) {
Charset charset = StandardCharsets.UTF_8;
MediaType contentType = exchange.getResponse().getHeaders().getContentType();
if (contentType != null && contentType.getCharset() != null) {
charset = contentType.getCharset();
}
MediaType mediaType = exchange.getResponse().getHeaders().getContentType();
Charset charset = (mediaType != null && mediaType.getCharset() != null ?
mediaType.getCharset() : StandardCharsets.UTF_8);
DataBufferFactory bufferFactory = exchange.getResponse().bufferFactory();
String eventLine = fragment.viewName() != null ? "event:" + fragment.viewName() + "\n" : "";
String eventLine = (fragment.viewName() != null ? "event:" + fragment.viewName() + "\n" : "");
DataBuffer prefix = encodeText(eventLine + "data:", charset, bufferFactory);
DataBuffer suffix = encodeText("\n\n", charset, bufferFactory);
return Flux.concat(Flux.just(prefix), fragmentContent, Flux.just(suffix));
Mono<DataBuffer> content = DataBufferUtils.join(fragmentFlux)
.map(dataBuffer -> {
String s = dataBuffer.toString(charset).replace("\n", "\ndata:");
return bufferFactory.wrap(s.getBytes(charset));
});
return Flux.concat(Flux.just(prefix), content, Flux.just(suffix));
}
private DataBuffer encodeText(String text, Charset charset, DataBufferFactory bufferFactory) {

View File

@@ -43,6 +43,7 @@ import org.springframework.web.reactive.accept.RequestedContentTypeResolver;
import org.springframework.web.reactive.result.view.script.ScriptTemplateConfigurer;
import org.springframework.web.reactive.result.view.script.ScriptTemplateViewResolver;
import org.springframework.web.testfixture.http.server.reactive.MockServerHttpRequest;
import org.springframework.web.testfixture.http.server.reactive.MockServerHttpResponse;
import org.springframework.web.testfixture.server.MockServerWebExchange;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
@@ -87,7 +88,14 @@ public class FragmentViewResolutionResultHandlerTests {
.then(Mono.defer(() -> exchange.getResponse().getBodyAsString()))
.block(Duration.ofSeconds(60));
assertThat(body).isEqualTo("<p>Hello Foo</p><p>Hello Bar</p>");
assertThat(exchange.getResponse().getHeaders().getContentType()).isEqualTo(MediaType.TEXT_HTML);
assertThat(body).isEqualTo("""
<p>
Hello Foo
</p>\
<p>
Hello Bar
</p>""");
}
@Test
@@ -98,6 +106,7 @@ public class FragmentViewResolutionResultHandlerTests {
.build();
MockServerWebExchange exchange = MockServerWebExchange.from(request);
MockServerHttpResponse response = exchange.getResponse();
HandlerResult result = new HandlerResult(
new Handler(),
@@ -106,15 +115,20 @@ public class FragmentViewResolutionResultHandlerTests {
new BindingContext());
String body = initHandler().handleResult(exchange, result)
.then(Mono.defer(() -> exchange.getResponse().getBodyAsString()))
.then(Mono.defer(response::getBodyAsString))
.block(Duration.ofSeconds(60));
assertThat(response.getHeaders().getContentType()).isEqualTo(MediaType.TEXT_EVENT_STREAM);
assertThat(body).isEqualTo("""
event:fragment1
data:<p>Hello Foo</p>
data:<p>
data: Hello Foo
data:</p>
event:fragment2
data:<p>Hello Bar</p>
data:<p>
data: Hello Bar
data:</p>
""");
}

View File

@@ -1,3 +1,6 @@
import org.springframework.web.reactive.result.view.script.*
"""<p>${i18n("hello")} $foo</p>"""
"""
|<p>
| ${i18n("hello")} $foo
|</p>""".trimMargin()

View File

@@ -1,3 +1,6 @@
import org.springframework.web.reactive.result.view.script.*
"""<p>${i18n("hello")} $bar</p>"""
"""
|<p>
| ${i18n("hello")} $bar
|</p>""".trimMargin()