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
This commit is contained in:
rstoyanchev
2024-08-05 15:53:12 +03:00
parent f5ed1b8f6d
commit b734156f32

View File

@@ -267,20 +267,24 @@ public class ViewResolutionResultHandler extends HandlerResultHandlerSupport imp
Mono.just(Collections.singletonList((View) view)));
}
else if (FragmentsRendering.class.isAssignableFrom(clazz)) {
ServerHttpResponse response = exchange.getResponse();
FragmentsRendering render = (FragmentsRendering) returnValue;
HttpStatusCode status = render.status();
if (status != null) {
exchange.getResponse().setStatusCode(status);
response.setStatusCode(status);
}
exchange.getResponse().getHeaders().putAll(render.headers());
response.getHeaders().putAll(render.headers());
bindingContext.updateModel(exchange);
StreamHandler streamHandler = getStreamHandler(exchange);
if (streamHandler != null) {
streamHandler.updateResponse(exchange);
}
Flux<Flux<DataBuffer>> renderFlux = render.fragments().concatMap(fragment ->
renderFragment(fragment, streamHandler, locale, bindingContext, exchange));
return exchange.getResponse().writeAndFlushWith(renderFlux);
return response.writeAndFlushWith(renderFlux);
}
else if (Model.class.isAssignableFrom(clazz)) {
model.addAllAttributes(((Model) returnValue).asMap());
@@ -299,7 +303,7 @@ public class ViewResolutionResultHandler extends HandlerResultHandlerSupport imp
viewsMono = resolveViews(getDefaultViewName(exchange), locale);
}
bindingContext.updateModel(exchange);
return viewsMono.flatMap(views -> render(views, model.asMap(), bindingContext, exchange));
return viewsMono.flatMap(views -> render(views, model.asMap(), null, bindingContext, exchange));
});
}
@@ -346,10 +350,16 @@ public class ViewResolutionResultHandler extends HandlerResultHandlerSupport imp
Mono.just(List.of(fragment.view())) :
resolveViews(fragment.viewName() != null ? fragment.viewName() : getDefaultViewName(exchange), locale));
return selectedViews.flatMap(views -> render(views, fragment.model(), bindingContext, mutatedExchange))
.then(Mono.fromSupplier(() -> (streamHandler != null ?
streamHandler.format(response.getBodyFlux(), fragment, exchange) :
response.getBodyFlux())));
Map<String, Object> model = fragment.model();
if (streamHandler != null) {
return selectedViews.flatMap(views -> render(views, model, MediaType.TEXT_HTML, bindingContext, mutatedExchange))
.then(Mono.fromSupplier(() -> streamHandler.format(response.getBodyFlux(), fragment, exchange)));
}
else {
return selectedViews.flatMap(views -> render(views, model, null, bindingContext, mutatedExchange))
.then(Mono.fromSupplier(response::getBodyFlux));
}
}
@Nullable
@@ -369,7 +379,8 @@ public class ViewResolutionResultHandler extends HandlerResultHandlerSupport imp
.orElseGet(() -> Conventions.getVariableNameForParameter(returnType));
}
private Mono<? extends Void> render(List<View> views, Map<String, Object> model,
private Mono<? extends Void> render(
List<View> views, Map<String, Object> model, @Nullable MediaType bestMediaType,
BindingContext bindingContext, ServerWebExchange exchange) {
for (View view : views) {
@@ -378,19 +389,20 @@ public class ViewResolutionResultHandler extends HandlerResultHandlerSupport imp
}
}
List<MediaType> mediaTypes = getMediaTypes(views);
MediaType bestMediaType;
try {
bestMediaType = selectMediaType(exchange, () -> mediaTypes);
}
catch (NotAcceptableStatusException ex) {
HttpStatusCode statusCode = exchange.getResponse().getStatusCode();
if (statusCode != null && statusCode.isError()) {
if (logger.isDebugEnabled()) {
logger.debug("Ignoring error response content (if any). " + ex.getReason());
}
return Mono.empty();
if (bestMediaType == null) {
try {
bestMediaType = selectMediaType(exchange, () -> mediaTypes);
}
catch (NotAcceptableStatusException ex) {
HttpStatusCode statusCode = exchange.getResponse().getStatusCode();
if (statusCode != null && statusCode.isError()) {
if (logger.isDebugEnabled()) {
logger.debug("Ignoring error response content (if any). " + ex.getReason());
}
return Mono.empty();
}
throw ex;
}
throw ex;
}
if (bestMediaType != null) {
for (View view : views) {
@@ -427,15 +439,23 @@ public class ViewResolutionResultHandler extends HandlerResultHandlerSupport imp
@Nullable
private Flux<DataBuffer> bodyFlux;
private final HttpHeaders headers;
@Nullable
private HttpHeaders headers;
BodySavingResponse(ServerHttpResponse delegate) {
super(delegate);
this.headers = new HttpHeaders(delegate.getHeaders()); // Ignore header changes
}
@Override
public HttpHeaders getHeaders() {
if (!super.getHeaders().containsKey(HttpHeaders.CONTENT_TYPE)) {
return super.getHeaders();
}
// Content-type is set, ignore further updates
if (this.headers == null) {
this.headers = new HttpHeaders();
this.headers.putAll(super.getHeaders());
}
return this.headers;
}
@@ -468,6 +488,11 @@ public class ViewResolutionResultHandler extends HandlerResultHandlerSupport imp
*/
boolean supports(ServerHttpRequest request);
/**
* Update the response before streaming, e.g. to set the content-type.
*/
void updateResponse(ServerWebExchange exchange);
/**
* Format the given fragment.
* @param fragmentContent the fragment serialized to data buffers
@@ -476,7 +501,6 @@ public class ViewResolutionResultHandler extends HandlerResultHandlerSupport imp
* @return the formatted fragment
*/
Flux<DataBuffer> format(Flux<DataBuffer> fragmentContent, Fragment fragment, ServerWebExchange exchange);
}
@@ -492,20 +516,14 @@ public class ViewResolutionResultHandler extends HandlerResultHandlerSupport imp
}
@Override
public Flux<DataBuffer> format(
Flux<DataBuffer> fragmentContent, Fragment fragment, ServerWebExchange exchange) {
public void updateResponse(ServerWebExchange exchange) {
MediaType mediaType = MediaType.TEXT_EVENT_STREAM;
Charset charset = getCharset(exchange.getRequest());
DataBufferFactory bufferFactory = exchange.getResponse().bufferFactory();
String eventLine = fragment.viewName() != null ? "event:" + fragment.viewName() + "\n" : "";
return Flux.concat(
Flux.just(encodeText(eventLine + "data:", charset, bufferFactory)),
fragmentContent,
Flux.just(encodeText("\n\n", charset, bufferFactory)));
mediaType = (charset != null ? new MediaType(mediaType, charset) : mediaType);
exchange.getResponse().getHeaders().setContentType(mediaType);
}
@Nullable
private Charset getCharset(ServerHttpRequest request) {
for (MediaType mediaType : request.getHeaders().getAccept()) {
if (mediaType.isCompatibleWith(MediaType.TEXT_EVENT_STREAM)) {
@@ -515,7 +533,26 @@ public class ViewResolutionResultHandler extends HandlerResultHandlerSupport imp
break;
}
}
return StandardCharsets.UTF_8;
return null;
}
@Override
public Flux<DataBuffer> format(
Flux<DataBuffer> fragmentContent, Fragment fragment, ServerWebExchange exchange) {
Charset charset = StandardCharsets.UTF_8;
MediaType contentType = exchange.getResponse().getHeaders().getContentType();
if (contentType != null && contentType.getCharset() != null) {
charset = contentType.getCharset();
}
DataBufferFactory bufferFactory = exchange.getResponse().bufferFactory();
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));
}
private DataBuffer encodeText(String text, Charset charset, DataBufferFactory bufferFactory) {