Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Progressive HTML rendering support [SPR-14981] #19547

Open
spring-projects-issues opened this issue Dec 5, 2016 · 21 comments
Open

Progressive HTML rendering support [SPR-14981] #19547

spring-projects-issues opened this issue Dec 5, 2016 · 21 comments
Labels
in: web Issues in web modules (web, webmvc, webflux, websocket) type: enhancement A general enhancement

Comments

@spring-projects-issues
Copy link
Collaborator

spring-projects-issues commented Dec 5, 2016

Sébastien Deleuze opened SPR-14981 and commented

We should provide a way to change flushing strategy without using ServerHttpResponse#writeAndFlushWith(Publisher<Publisher<DataBuffer>>) low level method.

That could be via supporting Publisher<Publisher<?>> return values or introducing a new annotation that could allow to flush the data after each element (could make sense when you serialize POJOs).


Issue Links:

3 votes, 8 watchers

@spring-projects-issues
Copy link
Collaborator Author

spring-projects-issues commented Feb 12, 2017

Sébastien Deleuze commented

Not sure we should do something here since after #19671 and this commit media type is used to enable flushing by element or not + performances have been improved. Annotation based programming model is quite high level and it is possible to use ServerWebExchange to implement custom low level flushing behavior, so I think it is probably better to wait more feedbacks before eventually doing something.

@spring-projects-issues
Copy link
Collaborator Author

Sébastien Deleuze commented

Notice that I have added an additional commit that enable flushing by element for application/stream+json mime type.

@spring-projects-issues
Copy link
Collaborator Author

Florian Stefan commented

I think it would be really useful to be able to control the flushing behaviour per controller method. A concrete example for this would be "Progressive HTML rendering". The idea is to split up the HTML into chunks and send each chunk to the browser as soon as it is available. So the browser can start rendering the page and also download additional resources without the overhead that AJAX via HTTP /1.1 introduces.

Facebook is calling this "BigPipe". We implemented this using the Play framework (see here for the basic ideas).

It would be really cool to do something similar with Spring 5 and Reactor. Having control over flushing behaviour would definitely help.

@spring-projects-issues
Copy link
Collaborator Author

Dave Syer commented

That's a really useful link (the Play talk). I think it highlights the point that actually, it's not really the controller that needs to know about flushing, but the view renderer. Thymeleaf already has a weak form of this (it's still a bit leaky, and you can only drive the flushing behaviour using a single Publisher). Maybe this is getting off topic from the original issue though (which was more about how to flush responses based on the body type).

@spring-projects-issues
Copy link
Collaborator Author

Sébastien Deleuze commented

We are getting a little bit out of the original topic indeed, but I think that's a good thing to understand the context and various alternatives to correctly evaluate what to do here.

I do think progressive HTML rendering would be a very useful feature. By progressive HTML rendering I mean being able to render sequentially templates based on multiple asynchronous model attributes without waiting the resolution of all these attributes, this is super useful for mobile and keep good SEO while avoiding users to wait few seconds to see anything. The most common use case would likely be request remote JSON HTTP endpoints with WebClient and begin to render and flush the HTML to the client asap the first async model attributes complete. WebFlux has the underlying architecture to support it, but there is some missing pieces when you consider the feature end to end. This is obviously not magic and model attributes usually coming with the more latency should be placed at the end of the template.

As pointed out by Dave Syer, in Spring world this should mainly be solved at view rendering level. Reactive template engine supporting progressive HTML rendering should override AbstractView#resolveAsyncAttributes to avoid pre-resolution of async attributes and pass them as they are to the engine to allow it listening async attributes completion. Since a single stream of HTML is produced, such template engine should determine what is the order or resolution based on the templates. There is currently not yet support for that even in Thymeleaf, so this is open for contribution and experimentation.

That said, it is also possible to implement that in a low level fashion by creating a Reactive pipeline that will call manually various codecs and output a Flux<DataBuffer> and call ServerHttpResponse#writeAndFlushWith(Publisher<Publisher<DataBuffer>>). This would be quite complicated, and I understand here the need to support for example flushing per element when returning Flux<String> (for example when you request remotely rendered webpages.

Arjen Poutsma Rossen Stoyanchev Brian Clozel Do you have any proposal about how we could specify this flushing on next element? Maybe a dedicated annotation or annotation attribute for the annotation programming model and dedicated methods or codec hint for the functional API ?

@spring-projects-issues
Copy link
Collaborator Author

Dave Syer commented

A Rendering kind of has some of the pieces that you need to encapsulate a fragment that needs to be flushed (e.g. comparing with the Play example where sections of the screen were built up and flushed as they became available). Maybe what I need is to be able to populate a Model with attributes of type Publisher<Rendering> (or something), and have them somehow flush the response when they are delivered?

@spring-projects-issues
Copy link
Collaborator Author

Sébastien Deleuze commented

Dave Syer If we are talking about the same use case, ie. being able to assemble fragment of HTML remotely rendered (for example by a remote Markdown rendering webservice or a remote template rendering server), it seems to me Mono<String> model attributes would be enough at least for a start (we can imagine having more feature to escape HTML, etc.). What is missing is "just" a Reactive template technology with a View implementation that overrides AbstractView#resolveAsyncAttributes and build itself the reactive pipeline in the right order based on templates and flush that data after each onComplete event (or onNext if we go as far as supporting rendering multiple Flux<T> like Thymeleaf DATA-DRIVEN mode). Thymeleaf 3.1 could be a good candidate, I will talk to Daniel Fernández about that.

@spring-projects-issues
Copy link
Collaborator Author

Sébastien Deleuze commented

I want also to clarify there are 2 use cases:

  • One purely server-side which is SEO friendly but where the aync elements will rendered in order. I think with proper advanced CSS styles and HTML structure, you could also achieve predictive displaying of your data regardless the order in the DOM, so the Reactive templating could under some conditions implement a "send first completed" behavior.
  • One implying JS code and Ajax or SSE to inject the HTML fragment as they are available. A single SSE stream opened to push fragment seems to me the best way to implement that, but regular Ajax request would be OK as well since we now have HTTP2 which supports many concurrent parallel requests.

I think both are interesting and cover different use cases.

@spring-projects-issues
Copy link
Collaborator Author

Dave Syer commented

I think there is a third possibility which is render a single stream and use JS to position the elements as they arrive (you don't need Ajax or SSE, so there's less code in the browser potentially). I'm interested in what the CSS options are though - maybe I'm already doing it, but with no custom CSS (browser seems more than willing to show partially complete HTML, e.g. table rows as they flush, without necessarily needing to see the end of the table).

FWIW I also don't think Publisher<String> is a very nice model (if you mean that the string is HTML), although it might be a stepping stone on the way. I want to be able to compose server side views with templates.

@spring-projects-issues
Copy link
Collaborator Author

Sébastien Deleuze commented

Dave Syer I don't understand the third way with single stream + JS, could you please give more details about it?

What would be the limitations of Publisher<String> model attributes regarding to server side views with template composition? What do you expect instead?

@spring-projects-issues
Copy link
Collaborator Author

Dave Syer commented

could you please give more details about it?

E.g.

<script id="foo" type="text/html">
// render content here
</script>
<script>
  $("#content").append($("#foo").html())
</script>

What would be the limitations of Publisher<String> model attributes

You would have to render all the strings yourself before adding them to the model, whereas Spring has a nice abstraction for doing that via a Rendering.

@spring-projects-issues
Copy link
Collaborator Author

Sébastien Deleuze commented

Thanks for the example of the third way (even if I am not sure that will work).

You would have to render all the strings yourself before adding them to the model, whereas Spring has a nice abstraction for doing that via a Rendering.

I still don't get it, to me there is 2 main use cases:

  • You are getting data asynchronously with something like WebClient receiving DTOs via JSON and you put your Flux<Foo> or Mono<Bar> in the model via Rendering for example. Data come remotely with latency but template rendering is done locally
  • You are getting HTML fragment already rendered from a remote Mardown webservice when you send POST request or a remote Thymeleaf or whatever other templating technology, you get various Mono<String> attributes that you can compose with your locale template engine via using Rendering as well.

In both cases the current Rendering is use to pass Mono and Flux model attributes. The missing piece is only the template rendering implementation that will not wait all async attributes to be completed to begin rendering.

Is your use case different?

@spring-projects-issues
Copy link
Collaborator Author

Dave Syer commented

Is your use case different?

I think so, but maybe I'm asking for too much. I'm trying to generalize the Thymeleaf features, and copy the guy using Play from LinkedIn to some extent, so the fact that those two examples exist gives me some confidence that we can do better.

If the remote call returns a Flux<Foo> and it can be rendered as a fragment, incrementally, then the HTTP response can be flushed as Foos arrive, not after they all are ready. Similarly the page might contain content from a Mono<Bar> and a Mono<Spam> and I want to render them as they arrive, as fragments (with a View and everything).

As I already said, I think this will require changes in the view template renderer. But hopefully Spring can provide a framework for doing that in a sensible way.

Remote rendering with a markup service is still a nice option. It just isn't the same, and gets a bit awkward if the rendering is actually not remote. Or maybe not. I'm happy to be proved wrong.

@spring-projects-issues
Copy link
Collaborator Author

Sébastien Deleuze commented

Yeah with Flux<Foo>, Foos could be rendered as they arrive.

Maybe at some point we will be able to provide some higher level constructs to help with that, but not sure since this is tricky and very related to each view rendering technology.

In any case, I still think the first steps are implementing these ideas in one of the template rendering running on top of WebFlux like the upcoming Thymeleaf 3.1, and on our side provide a way to flush data after each onNext in order to allow developers doing powerful things programmatically.

@spring-projects-issues
Copy link
Collaborator Author

Dave Syer commented

So this works quite well already:

@GetMapping("/flux")
@ResponseBody
Mono<Void> flux(ServerWebExchange exchange)
          throws Exception {
     return exchange.getResponse().writeAndFlushWith(Flux
               .just("<html>\n<body>\n", "<h2>Demo</h2>\n", "<span>Hello</span>\n",
                                 "</body></html>\n")
               .delayElements(Duration.ofSeconds(1))
               .map(body -> buffer(exchange, body)));
}
private Publisher<DataBuffer> buffer(ServerWebExchange exchange, String body) {
     return Mono.just(exchange.getResponse().bufferFactory().allocateBuffer()
               .write(body.getBytes()));
}

Kind of rough, but it shows the principle, and the browser renders the page progressively. Question: it doesn't work if you don't end the fragments with \n (only complete lines are flushed). Is that expected? Why?

@spring-projects-issues
Copy link
Collaborator Author

Sébastien Deleuze commented

I guess it depends on browsers DOM rendering implementation and about the DOM structure of the page you flush. On my Ubuntu laptop, your example works both with or without \n with Chrome 61 and never works with Firefox 56.

@spring-projects-issues
Copy link
Collaborator Author

Dave Syer commented

Indeed. I was testing with curl (where the \n seem to be important). Chrome works without them.

@spring-projects-issues
Copy link
Collaborator Author

spring-projects-issues commented Oct 19, 2017

Daniel Fernández commented

I just want to add my two cents, from the Thymeleaf perspective. Sorry for the length of the text...

First of all and for better context, let me clarify the three operation modes currently offered by Thymeleaf when used with Spring WebFlux:

  • FULL, the default. Pages are fully rendered in-memory in only one engine execution, full output is written to a single DataBuffer object, and sent to the output channels in a single call (simplifying).

  • CHUNKED, enabled in Spring Boot by setting the spring.thymeleaf.reactive.max-chunk-size configuration property. Thymeleaf will stop each time it produces output (HTML) of a size equal to the maximum chunk established. Each produced output chunk will be sent to the server's output channels in the form of a DataBuffer object, the output channels will be flushed, and Thymeleaf will wait (non-actively) for a new request(...) to come from downstream in order to resume execution of the template.

  • DATA-DRIVEN, enabled by adding one model attribute (and only one) implementing the IReactiveDataDriverContextVariable interface and wrapping a Publisher<?> data stream, for which there should be an iteration (th:each) in the template. This makes Thymeleaf work as a subscriber on that Publisher<?> and execute the template engine each time an element is generated by that Publisher<?> (actually, by default it establishes a buffer of 10 elements and executes for each buffer of 10 elements, but this can be configured and make it execute for each element). Each execution will result in a DataBuffer, which will be sent to the server's output channels and flushed. If a max chunk size is established (as explained for the CHUNKED mode), then none of these DataBuffer objects will be allowed to exceed that size, so for each item generated by the underlying Publisher<?> several DataBuffer objects of the max size could be sent to the server's output channels (and each one would be flushed).

Note that the flush operations mentioned above are performed by means of using response.writeAndFlushWith(...) at the ThymeleafReactiveView class. If I'm right you mention in this ticket the possibility of providing view engines with some kind of additional capability for flushing output after each onNext() from the underlying data stream, but that's something Thymeleaf already can do so I'm probably missing something... could you please give a bit more detail on what you mean with this?

Also note that, in all of the template modes above, all reactive data stream attributes in the model at the time of view-layer execution (except the data-driver model attribute in DATA-DRIVEN mode) will be resolved by Spring WebFlux by means of AbstractView#resolveAsyncAttributes(...) before Thymeleaf actually starts to execute. So a Thymeleaf view layer can be called with any number of reactive data streams in the model, but only one at most can end up driving the rendering of HTML as its elements are published. The rest of them will be fully resolved before HTML starts being generated at all. This is per-construction in Thymeleaf 3.0.

Unfortunately there has been no time yet to create a proper tutorial explaining all the features of the integration between Thymeleaf and Spring WebFlux, so the available documentation detail is scattered in GitHub tickets and JavaDoc, like ISpringWebFluxTemplateEngine or also my talk last May at Spring I/O: "Getting Thymeleaf Ready for Spring 5 and Reactive"

Now this said, I'd like to talk about the scenarios you describe for UI composition, and how I see them from the Thymeleaf perspective. If I understand correctly, you are talking about three different scenarios that approach the browser-side rendering of a complex HTML interface combining multiple (not just one) reactive data streams, or more specifically combining the fragments of HTML rendered for data coming from multiple reactive data streams.

I stress the fact of having multiple data streams because I believe the "single data stream scenario" is already covered today out-of-the-box by Thymeleaf using the DATA-DRIVEN mode as explained above.

Summary of scenarios being proposed

In summary (and in my understanding), these scenarios would be:

  • Scenario 1. One text/html request only, using JavaScript, allowing unordered rendering of fragments. This is the Facebook BigPipe mechanism, explained (in a simplified way) in the Play video linked above in a previous comment.

  • Scenario 2. Two requests: one text/html, a second one text/event-stream (SSE). Using JavaScript. Allowing unordered rendering of fragments. This is a scenario I proposed to Sébastien based on features I added to Thymeleaf 3.0.8 (more on this later).

  • Scenario 3. Pure server-side, no JavaScript involved. One single text/html request. Transmission of the async HTML fragments towards the browser performed in order (no unordered rendering allowed).

Note I'm discarding from this discussion the scenario with multiple full-JavaScript based requests (one request per async fragment, using AJAX, SSE or even WebSockets), as I believe the key aim here is trying to reduce the total amount of requests required for UI composition.

+Possible implementations based on Thymeleaf+

Let's see this in a bit deeper detail and adding Thymeleaf's current capabilities (version 3.0.8) to the equation. As you will see, the trick here will be that we will try to overcome the limitation of Thymeleaf allowing only one reactive data stream per execution by combining multiple Thymeleaf executions into the same response, one for each fragment. Also, we will not be using ThymeleafReactiveView, so we will be first-hand responsible for composing the response and configuring its flushing behaviour from the controller.

Scenario 1 (one request, with JavaScript)

Scenario 1 would consist of a single request with a text/html response. The first chunk sent to the browser would contain the beginning of the HTML document with: 1. <head>, 2. An unclosed <body> containing the general skeleton of the page including placeholders for all the involved fragments, and 3. Some JavaScript code able to position a fragment of HTML (a DOM node, say, a <div>) in one of the defined placeholders by means of checking its class or id. For the sake of the example, let's call this JS function placeFragment(...).

At the controller (returning Mono<Void>) Thymeleaf –specifically a SpringWebFluxTemplateEngine instance– would be directly called once per UI async fragment, in DATA-DRIVEN mode on the data stream corresponding to each fragment, and each of these calls would return a Flux<DataBuffer>. Each element being published by the data-drivers would make Thymeleaf render a piece of HTML delimited by some kind of container (e.g. a <div> block) with some specific class or id values, and after the <div> container would come a small piece of JavaScript that would call the placeFragment(...) function passing the container's node as argument.

All these Thymeleaf-ised Flux<DataBuffer> objects then can be Flux#merge-d into a new Flux<DataBuffer> that would be able to emit all UI fragments unordered. Then this merged stream can be Flux#concat-ed with two Mono<DataBuffer> objects: one that would previously write a DataBuffer containing the HTML skeleton (plus the code for the placeFragment() JavaScript function) and another one that would close the </body> and </html>. The resulting Flux<DataBuffer> would be used in a response.writeAndFlushWith() call, and that should be it.

All of this is of course a huge simplification, but it would be something similar to:

@RequestMapping("/scenario1")
public Mono<Void> scenario1(final ServerWebExchange exchange) {
	
	Flux<DataBuffer> asyncHTMLFragment1 = this.thymeleafEngine.process("asyncfragment1", ...);
	Flux<DataBuffer> asyncHTMLFragment2 = this.thymeleafEngine.process("asyncfragment2", ...);
	Flux<DataBuffer> asyncHTMLFragment3 = ...

	Flux<DataBuffer> fragments = Flux.merge(asyncHTMLFragment1, asyncHTMLFragment2, ...);

	Mono<DataBuffer> headerAndSkeleton = ...;
	Mono<DataBuffer> closing = ...;

	Flux<DataBuffer> page = Flux.concat(headerAndSkeleton, fragments, closing);

	return exchange.getResponse().writeAndFlushWith(page.window(1));

}

Things to note:

  • Obviously, this is not a general solution. Specific controller code needs to be written for each page for which we want to compose UI this way. Seems reasonably generalisable though.
  • This solution requires a considerable amount of JavaScript, which I'm omitting on purpose. And things like each independent fragment contributing its own CSS could make things quite complex.\

Scenario 2 (two requests, using SSE)

Scenario 2 would be a bit similar to Scenario 1, but we would have two requests. The first one would have a text/html response consisting of a complete HTML document, containing:

  • The whole (<html> to </html>) skeleton of the page with placeholders for each fragment.
  • An amount of JavaScript declaring an EventSource object which performs a second request (text/event-stream, but receiving SSE messages whose payload is HTML, not the typical JSON) and declares a callback for each received SSE message that looks for a prefix in the id: or event: fields of the SSE message and, depending on the value of this prefix, applies the HTML received in the SSE message to the corresponding placeholder in the page skeleton.

At the controller for the second call (SSE), returning Mono<Void>, Thymeleaf (again a SpringWebFluxTemplateEngine object) can be directly called once per UI fragment, configuring the engine to create SSE events as output. These executions would be in DATA-DRIVEN mode, and when configuring the data-driver variable for each execution, a different SSE prefix would be specified for each one (that's done in the ReactiveDataDriverContextVariable constructor, a new feature in Thymeleaf 3.0.8).

So we would have as a result a number of Flux<DataBuffer> objects that would produce SSE events for each of our reactive data streams, each of them with a different prefix so that they can be easily discriminated at the browser side by the EventSource callback mentioned above.

We would then Flux#merge all these Flux<DataBuffer> and instruct the response to .writeAndFlushWith(...), similar to the previous example:

@RequestMapping("/scenario2")
public Mono<Void> scenario2(final ServerWebExchange exchange) {
	
	// All these "process" calls will be configured to generate SSE with HTML payload and different prefix
	Flux<DataBuffer> asyncHTMLFragment1 = this.thymeleafEngine.process("asyncfragment1", ...);
	Flux<DataBuffer> asyncHTMLFragment2 = this.thymeleafEngine.process("asyncfragment2", ...);
	Flux<DataBuffer> asyncHTMLFragment3 = ...

	Flux<DataBuffer> fragments = Flux.merge(asyncHTMLFragment1, asyncHTMLFragment2, ...);

	return exchange.getResponse().writeAndFlushWith(fragments.window(1));

}

Things to note:

  • Again, not a general solution. Specific code is needed for each page we want to compose.
  • Needs JavaScript, but IMHO quite simpler and cleaner than the one for Scenario 1.
  • Involves two requests, not just one. Still, way better than one-request-per-fragment.
  • Support for SSE in MS Edge is tricky (no support, needs a polyfill).

Scenario 3 (one request, no JavaScript)

Scenario 3 removes JavaScript from the scenario, and so my understanding is that it removes in practice the possibility that fragments can reach the browser unordered. Order will be a must, unless we are able to place absolutely everything in its position by means of mere CSS -- which I'm not completely sure about, but I'm no CSS expert. I assume it will depend on the specifics of the page.

Now, once this limitation is established, this scenario would be somewhat similar to Scenario 1. We would call Thymeleaf multiple times in our controller, Flux#concat-ing the different parts in the right order (header, async fragment 1, inner 1-2 fragment, async fragment 2, ..., footer) and finally we would instruct the response to write and flush on the resulting Flux<DataBuffer>.

The main (and uncomfortable) difference here is that we would have to declare Thymeleaf Flux<DataBuffer> executions (non-DATA-DRIVEN in this case) for each of the non-asynchronous inner fragments of HTML appearing between asynchronous fragments. Not pretty at all, I admit.

And of course we would not have the possibility of allowing all these HTML fragments to be sent to the browser unordered. This is, unless CSS magic allowed this. But this is a limitation posed by the scenario definition itself, not by our use of Thymeleaf.

Our code would look like this. Note how we have header, footer and all the inner fragments in the same template file (base.html), and we take advantage of Thymeleaf's capability to process partial template fragments:

@RequestMapping("/scenario3")
public Mono<Void> scenario3(final ServerWebExchange exchange) {

        // Note header, inner fragments and footer come from the same template file (base.html)

	Flux<DataBuffer> header = this.thymeleafEngine.process("base :: header", ...);	
	Flux<DataBuffer> asyncHTMLFragment1 = this.thymeleafEngine.process("asyncfragment1", ...);
        Flux<DataBuffer> inner12 = this.thymeleafEngine.process("base :: inner12", ...);
	Flux<DataBuffer> asyncHTMLFragment2 = this.thymeleafEngine.process("asyncfragment2", ...);
        Flux<DataBuffer> inner23 = this.thymeleafEngine.process("base :: inner23", ...);
	Flux<DataBuffer> asyncHTMLFragment3 = ...
        ...
        Flux<DataBuffer> footer = this.thymeleafEngine.process("base :: footer", ...);

	Flux<DataBuffer> page = 
                Flux.concat(header, asyncHTMLFragment1, inner12, asyncHTMLFragment2, inner23, ..., footer);

	return exchange.getResponse().writeAndFlushWith(page.window(1));

}

Things to note:

  • Specific code needed, not a general solution, like in all other cases.
  • No JavaScript (great) but unordered rendering might be trickier or impossible (not great, but maybe fair enough?).
  • More complex to maintain due to inner fragments, and less resilient to design changes.

Looking forward (Thymeleaf 3.1)

So in general, as you see, my idea here is that these scenarios could be implemented today with Thymeleaf plus an amount of additional Java/JavaScript application code, but they would not be a general solution or constitute any kind of UI composition framework. Note how we are not using ThymeleafReactiveView in the examples above, so in some sense we are working around the view-layer infrastructure in WebFlux. Which isn't great, but IMHO also not a big deal for special cases like this.

Also, the limitation of only being able to use one data-driver model attribute per DATA-DRIVEN execution could be OK if we can match every one of our async data streams to a different async UI fragment — after all async UI fragments are many times developed as separate HTML templates, so processing them as separate Thymeleaf executions makes sense. But this would definitely be an issue if we need to have other additional async model attributes that we don't want to be resolved before template starts rendering (AbstractView#resolveAsyncAttributes(...)), and which we don't want to give the status of "asynchronous UI fragments" either (so we don't want to make them data-drivers of a separate Thymeleaf execution).

As Sébastien mentioned, this last point is one of the topics Thymeleaf 3.1 will try to focus on: the ability to execute a template without the need resolve all-but-one of the async model attributes before execution, therefore effectively allowing multi-data-driver DATA-DRIVEN execution. And therefore allowing the implementation of Scenario 3 out of the box and in a single Thymeleaf execution.

Note however that development of the 3.1 branch has not started yet :)

@spring-projects-issues
Copy link
Collaborator Author

Dave Syer commented

Thanks, Daniel, that is a wonderful deep summary of all that we left unsaid in the above (with a slant to Thymeleaf of course). It's fantastic that Thymeleaf 3 supports these patterns, and even better that you are thinking of improvements in 3.1.

I share some of the reservations that you express about the current implementation. It feels to me like the controller should not have to know about these things, so I am looking for a solution that can be expressed in the view layer. I am also looking for a toolbox that can be used to extend these features to other template engines (Thymeleaf is excellent, but it's not everyone's choice).

I'm not sure I really understand the all-but-one limitation either, but since that is also a Thymeleaf specific feature we should discuss it in a more specific context.

@spring-projects-issues
Copy link
Collaborator Author

Rossen Stoyanchev commented

I was testing with curl (where the \n seem to be important)

dsyer have you tried with -N (no buffering)?

Note that the flush operations mentioned above are performed by means of using response.writeAndFlushWith(...) at the ThymeleafReactiveView class. If I'm right you mention in this ticket the possibility of providing view engines with some kind of additional capability for flushing output after each onNext() from the underlying data stream, but that's something Thymeleaf already can do so I'm probably missing something...

Indeed I think this whole discussion has little (or nothing) to do with providing more control over flushing. Flushing control makes sense for @ResponseBody-style methods but in the case of a templates, it's the view engine that needs to control flushing and for that the available option on ServerHttpResponse seem fine.

Daniel Fernández, I think in your second example with SSE, couldn't you just make that an @ResponseBody returning a Flux<DataBuffer> or Flux<ServerSentEvent>? It would be a minor simplification indeed, simply avoiding the need to call ServerHttpResponse directly.

Overall what I see is that from a Spring WebFlux perspective we could enable this concept of a view composed of multiple other views (HTML fragments) so that the controller doesn't have to do it all. At a high level we need to return a top-level Rendering, with its own view + model, and then some nested Rendering's each with its own view + model. Then either the ViewResolutionResultHandler could handle this, or perhaps it would be somehow delegated to view technologies that can support this. We need to try out to know more.

@spring-projects-issues
Copy link
Collaborator Author

Daniel Fernández commented

Daniel Fernández, I think in your second example with SSE, couldn't you just make that an @ResponseBody returning a Flux<DataBuffer> or Flux<ServerSentEvent>? It would be a minor simplification indeed, simply avoiding the need to call ServerHttpResponse directly.

In all three scenarios I believe that calls to request.getResponse().writeAndFlush(stream.window(1)) could be replaced by simply returning a Flux<DataBuffer> in a @ResponseBody-annotated controller as long as the higher-level infrastructure applied precisely that behaviour to the returned Flux, i.e. to flush after each of the DataBuffer is produced...

But note that would be Flux<DataBuffer> and not Flux<ServerSentEvent>, as Thymeleaf has its own infrastructure for formatting and outputting SSE events and controlling its own execution by means of monitoring the size of the written output. Also, if I remember correctly when I implemented this some things like applying specific id: or event: fields to the generated SSE messages were not available at the out-of-the-box WebFlux SSE infrastructure. And here we are not talking about SSE events containing JSON, but HTML.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: web Issues in web modules (web, webmvc, webflux, websocket) type: enhancement A general enhancement
Projects
None yet
Development

No branches or pull requests

2 participants