Skip to content

Commit b734156

Browse files
committed
Update WebFlux fragment stream rendering
Thymeleaf has its own special handling for SSE that gets in the way of fragment rendering. This is why we need to set the response content-type before streaming, and then pass text/html to the View for rendering each fragment. See gh-33194
1 parent f5ed1b8 commit b734156

File tree

1 file changed

+72
-35
lines changed

1 file changed

+72
-35
lines changed

spring-webflux/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java

+72-35
Original file line numberDiff line numberDiff line change
@@ -267,20 +267,24 @@ else if (Rendering.class.isAssignableFrom(clazz)) {
267267
Mono.just(Collections.singletonList((View) view)));
268268
}
269269
else if (FragmentsRendering.class.isAssignableFrom(clazz)) {
270+
ServerHttpResponse response = exchange.getResponse();
270271
FragmentsRendering render = (FragmentsRendering) returnValue;
271272
HttpStatusCode status = render.status();
272273
if (status != null) {
273-
exchange.getResponse().setStatusCode(status);
274+
response.setStatusCode(status);
274275
}
275-
exchange.getResponse().getHeaders().putAll(render.headers());
276+
response.getHeaders().putAll(render.headers());
276277
bindingContext.updateModel(exchange);
277278

278279
StreamHandler streamHandler = getStreamHandler(exchange);
280+
if (streamHandler != null) {
281+
streamHandler.updateResponse(exchange);
282+
}
279283

280284
Flux<Flux<DataBuffer>> renderFlux = render.fragments().concatMap(fragment ->
281285
renderFragment(fragment, streamHandler, locale, bindingContext, exchange));
282286

283-
return exchange.getResponse().writeAndFlushWith(renderFlux);
287+
return response.writeAndFlushWith(renderFlux);
284288
}
285289
else if (Model.class.isAssignableFrom(clazz)) {
286290
model.addAllAttributes(((Model) returnValue).asMap());
@@ -299,7 +303,7 @@ else if (View.class.isAssignableFrom(clazz)) {
299303
viewsMono = resolveViews(getDefaultViewName(exchange), locale);
300304
}
301305
bindingContext.updateModel(exchange);
302-
return viewsMono.flatMap(views -> render(views, model.asMap(), bindingContext, exchange));
306+
return viewsMono.flatMap(views -> render(views, model.asMap(), null, bindingContext, exchange));
303307
});
304308
}
305309

@@ -346,10 +350,16 @@ private Mono<Flux<DataBuffer>> renderFragment(
346350
Mono.just(List.of(fragment.view())) :
347351
resolveViews(fragment.viewName() != null ? fragment.viewName() : getDefaultViewName(exchange), locale));
348352

349-
return selectedViews.flatMap(views -> render(views, fragment.model(), bindingContext, mutatedExchange))
350-
.then(Mono.fromSupplier(() -> (streamHandler != null ?
351-
streamHandler.format(response.getBodyFlux(), fragment, exchange) :
352-
response.getBodyFlux())));
353+
Map<String, Object> model = fragment.model();
354+
355+
if (streamHandler != null) {
356+
return selectedViews.flatMap(views -> render(views, model, MediaType.TEXT_HTML, bindingContext, mutatedExchange))
357+
.then(Mono.fromSupplier(() -> streamHandler.format(response.getBodyFlux(), fragment, exchange)));
358+
}
359+
else {
360+
return selectedViews.flatMap(views -> render(views, model, null, bindingContext, mutatedExchange))
361+
.then(Mono.fromSupplier(response::getBodyFlux));
362+
}
353363
}
354364

355365
@Nullable
@@ -369,7 +379,8 @@ private String getNameForReturnValue(MethodParameter returnType) {
369379
.orElseGet(() -> Conventions.getVariableNameForParameter(returnType));
370380
}
371381

372-
private Mono<? extends Void> render(List<View> views, Map<String, Object> model,
382+
private Mono<? extends Void> render(
383+
List<View> views, Map<String, Object> model, @Nullable MediaType bestMediaType,
373384
BindingContext bindingContext, ServerWebExchange exchange) {
374385

375386
for (View view : views) {
@@ -378,19 +389,20 @@ private Mono<? extends Void> render(List<View> views, Map<String, Object> model,
378389
}
379390
}
380391
List<MediaType> mediaTypes = getMediaTypes(views);
381-
MediaType bestMediaType;
382-
try {
383-
bestMediaType = selectMediaType(exchange, () -> mediaTypes);
384-
}
385-
catch (NotAcceptableStatusException ex) {
386-
HttpStatusCode statusCode = exchange.getResponse().getStatusCode();
387-
if (statusCode != null && statusCode.isError()) {
388-
if (logger.isDebugEnabled()) {
389-
logger.debug("Ignoring error response content (if any). " + ex.getReason());
392+
if (bestMediaType == null) {
393+
try {
394+
bestMediaType = selectMediaType(exchange, () -> mediaTypes);
395+
}
396+
catch (NotAcceptableStatusException ex) {
397+
HttpStatusCode statusCode = exchange.getResponse().getStatusCode();
398+
if (statusCode != null && statusCode.isError()) {
399+
if (logger.isDebugEnabled()) {
400+
logger.debug("Ignoring error response content (if any). " + ex.getReason());
401+
}
402+
return Mono.empty();
390403
}
391-
return Mono.empty();
404+
throw ex;
392405
}
393-
throw ex;
394406
}
395407
if (bestMediaType != null) {
396408
for (View view : views) {
@@ -427,15 +439,23 @@ private static class BodySavingResponse extends ServerHttpResponseDecorator {
427439
@Nullable
428440
private Flux<DataBuffer> bodyFlux;
429441

430-
private final HttpHeaders headers;
442+
@Nullable
443+
private HttpHeaders headers;
431444

432445
BodySavingResponse(ServerHttpResponse delegate) {
433446
super(delegate);
434-
this.headers = new HttpHeaders(delegate.getHeaders()); // Ignore header changes
435447
}
436448

437449
@Override
438450
public HttpHeaders getHeaders() {
451+
if (!super.getHeaders().containsKey(HttpHeaders.CONTENT_TYPE)) {
452+
return super.getHeaders();
453+
}
454+
// Content-type is set, ignore further updates
455+
if (this.headers == null) {
456+
this.headers = new HttpHeaders();
457+
this.headers.putAll(super.getHeaders());
458+
}
439459
return this.headers;
440460
}
441461

@@ -468,6 +488,11 @@ private interface StreamHandler {
468488
*/
469489
boolean supports(ServerHttpRequest request);
470490

491+
/**
492+
* Update the response before streaming, e.g. to set the content-type.
493+
*/
494+
void updateResponse(ServerWebExchange exchange);
495+
471496
/**
472497
* Format the given fragment.
473498
* @param fragmentContent the fragment serialized to data buffers
@@ -476,7 +501,6 @@ private interface StreamHandler {
476501
* @return the formatted fragment
477502
*/
478503
Flux<DataBuffer> format(Flux<DataBuffer> fragmentContent, Fragment fragment, ServerWebExchange exchange);
479-
480504
}
481505

482506

@@ -492,20 +516,14 @@ public boolean supports(ServerHttpRequest request) {
492516
}
493517

494518
@Override
495-
public Flux<DataBuffer> format(
496-
Flux<DataBuffer> fragmentContent, Fragment fragment, ServerWebExchange exchange) {
497-
519+
public void updateResponse(ServerWebExchange exchange) {
520+
MediaType mediaType = MediaType.TEXT_EVENT_STREAM;
498521
Charset charset = getCharset(exchange.getRequest());
499-
DataBufferFactory bufferFactory = exchange.getResponse().bufferFactory();
500-
501-
String eventLine = fragment.viewName() != null ? "event:" + fragment.viewName() + "\n" : "";
502-
503-
return Flux.concat(
504-
Flux.just(encodeText(eventLine + "data:", charset, bufferFactory)),
505-
fragmentContent,
506-
Flux.just(encodeText("\n\n", charset, bufferFactory)));
522+
mediaType = (charset != null ? new MediaType(mediaType, charset) : mediaType);
523+
exchange.getResponse().getHeaders().setContentType(mediaType);
507524
}
508525

526+
@Nullable
509527
private Charset getCharset(ServerHttpRequest request) {
510528
for (MediaType mediaType : request.getHeaders().getAccept()) {
511529
if (mediaType.isCompatibleWith(MediaType.TEXT_EVENT_STREAM)) {
@@ -515,7 +533,26 @@ private Charset getCharset(ServerHttpRequest request) {
515533
break;
516534
}
517535
}
518-
return StandardCharsets.UTF_8;
536+
return null;
537+
}
538+
539+
@Override
540+
public Flux<DataBuffer> format(
541+
Flux<DataBuffer> fragmentContent, Fragment fragment, ServerWebExchange exchange) {
542+
543+
Charset charset = StandardCharsets.UTF_8;
544+
MediaType contentType = exchange.getResponse().getHeaders().getContentType();
545+
if (contentType != null && contentType.getCharset() != null) {
546+
charset = contentType.getCharset();
547+
}
548+
549+
DataBufferFactory bufferFactory = exchange.getResponse().bufferFactory();
550+
551+
String eventLine = fragment.viewName() != null ? "event:" + fragment.viewName() + "\n" : "";
552+
DataBuffer prefix = encodeText(eventLine + "data:", charset, bufferFactory);
553+
DataBuffer suffix = encodeText("\n\n", charset, bufferFactory);
554+
555+
return Flux.concat(Flux.just(prefix), fragmentContent, Flux.just(suffix));
519556
}
520557

521558
private DataBuffer encodeText(String text, Charset charset, DataBufferFactory bufferFactory) {

0 commit comments

Comments
 (0)