Support Flux<ServerSentEvent<Fragment>> in WebFlux

Closes gh-33975
This commit is contained in:
rstoyanchev
2024-12-10 17:31:39 +00:00
parent 640e570583
commit 3b95d2c449
3 changed files with 120 additions and 45 deletions

View File

@@ -35,7 +35,9 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType;
import org.springframework.http.MediaType;
import org.springframework.http.codec.ServerSentEvent;
import org.springframework.web.reactive.BindingContext;
import org.springframework.web.reactive.HandlerResult;
import org.springframework.web.reactive.accept.HeaderContentTypeResolver;
@@ -99,27 +101,11 @@ public class FragmentViewResolutionResultHandlerTests {
}
@Test
void renderSse() {
MockServerHttpRequest request = MockServerHttpRequest.get("/")
.accept(MediaType.TEXT_EVENT_STREAM)
.acceptLanguageAsLocales(Locale.ENGLISH)
.build();
void renderFragmentStream() {
MockServerWebExchange exchange = MockServerWebExchange.from(request);
MockServerHttpResponse response = exchange.getResponse();
HandlerResult result = new HandlerResult(
new Handler(),
Flux.just(fragment1, fragment2).subscribeOn(Schedulers.boundedElastic()),
testSse(Flux.just(fragment1, fragment2),
on(Handler.class).resolveReturnType(Flux.class, Fragment.class),
new BindingContext());
String body = initHandler().handleResult(exchange, result)
.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>
data: Hello Foo
@@ -133,6 +119,55 @@ public class FragmentViewResolutionResultHandlerTests {
""");
}
@Test
void renderServerSentEventFragmentStream() {
ServerSentEvent<Fragment> event1 = ServerSentEvent.builder(fragment1).id("id1").event("event1").build();
ServerSentEvent<Fragment> event2 = ServerSentEvent.builder(fragment2).id("id2").event("event2").build();
MethodParameter returnType = on(Handler.class).resolveReturnType(
Flux.class, ResolvableType.forClassWithGenerics(ServerSentEvent.class, Fragment.class));
testSse(Flux.just(event1, event2), returnType,
"""
id:id1
event:event1
data:<p>
data: Hello Foo
data:</p>
id:id2
event:event2
data:<p>
data: Hello Bar
data:</p>
""");
}
private void testSse(Flux<?> dataFlux, MethodParameter returnType, String output) {
MockServerHttpRequest request = MockServerHttpRequest.get("/")
.accept(MediaType.TEXT_EVENT_STREAM)
.acceptLanguageAsLocales(Locale.ENGLISH)
.build();
MockServerWebExchange exchange = MockServerWebExchange.from(request);
MockServerHttpResponse response = exchange.getResponse();
HandlerResult result = new HandlerResult(
new Handler(),
dataFlux.subscribeOn(Schedulers.boundedElastic()),
returnType,
new BindingContext());
String body = initHandler().handleResult(exchange, result)
.then(Mono.defer(response::getBodyAsString))
.block(Duration.ofSeconds(60));
assertThat(response.getHeaders().getContentType()).isEqualTo(MediaType.TEXT_EVENT_STREAM);
assertThat(body).isEqualTo(output);
}
private ViewResolutionResultHandler initHandler() {
AnnotationConfigApplicationContext context =
@@ -155,6 +190,8 @@ public class FragmentViewResolutionResultHandlerTests {
Flux<Fragment> renderFlux() { return null; }
Flux<ServerSentEvent<Fragment>> renderSseFlux() { return null; }
List<Fragment> renderList() { return null; }
}

View File

@@ -41,6 +41,7 @@ import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.codec.ServerSentEvent;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.lang.Nullable;
import org.springframework.ui.ConcurrentModel;
@@ -84,6 +85,9 @@ class ViewResolutionResultHandlerTests {
testSupports(on(Handler.class).resolveReturnType(FragmentsRendering.class));
testSupports(on(Handler.class).resolveReturnType(Flux.class, Fragment.class));
testSupports(on(Handler.class).resolveReturnType(
Flux.class, ResolvableType.forClassWithGenerics(ServerSentEvent.class, Fragment.class)));
testSupports(on(Handler.class).resolveReturnType(List.class, Fragment.class));
testSupports(on(Handler.class).resolveReturnType(
Mono.class, ResolvableType.forClassWithGenerics(List.class, Fragment.class)));
@@ -457,6 +461,7 @@ class ViewResolutionResultHandlerTests {
FragmentsRendering fragmentsRendering() { return null; }
Flux<Fragment> fragmentFlux() { return null; }
Flux<ServerSentEvent<Fragment>> fragmentServerSentEventFlux() { return null; }
Mono<List<Fragment>> monoFragmentList() { return null; }
List<Fragment> fragmentList() { return null; }