How can I make a server push with webflux (Netty for example)?

I know how to do with Spring MVC using PushBuilder (Servlet 4.0).

Comment From: bclozel

We don't have an API for that in WebFlux for now.

We should probably wait for this to be supported in Reactor Netty. That setting seems to be disabled for now (see https://github.com/reactor/reactor-netty/pull/1325) and I can't find an issue there. Could you create one and explain your use case there?

Thanks!

Comment From: userquin

@bclozel https://github.com/reactor/reactor-netty/issues/1352

Comment From: userquin

It will be also a great opportunite to add a server push manifest server on spring boot side: see this Server push manifest and HTTP 2.0 push on App Engine.

One can build an server push manifest file from the client and then provide it to spring boot...

Something similar to resources, for example serverPushManifest with a Resource.

Comment From: userquin

@bclozel it seems it is not necessary, just adding Link header will work on both, MVC also on WebFlux:

MVC Interceptor

    override fun preHandle(request: HttpServletRequest, response: HttpServletResponse, handler: Any): Boolean {
        val push = with(request.requestURI) {
            when (this) {
                "/index.html" -> listOf(
                        "_assets/style.5676cd4c.css",
                        "_assets/index.bfb29383.js"
                )
                "/env.html" -> listOf(
                        "_assets/style.5676cd4c.css",
                        "_assets/TestEnv.675482fe.js",
                        "_assets/index.bfb29383.js"
                )
                "/custom-blocks.html" -> listOf(
                        "_assets/style.5676cd4c.css",
                        "_assets/One.ce379106.js",
                        "_assets/Two.1f6c8df8.js",
                        "_assets/TestCustomBlocks.4c398f20.js",
                        "_assets/index.bfb29383.js"
                )
                "/dynamic-import.html" -> listOf(
                        "_assets/style.5676cd4c.css",
                        "_assets/TestDynamicImport.12f7cca2.js",
                        "_assets/index.bfb29383.js"
                )
                else -> emptyList()
            }
        }
        response.addHeader("Link", push.joinToString(", ") {
            when {
                it.endsWith(".css") -> "</$it>;rel=preload;as=style"
                /* assume there is only css and js */
                else -> "</$it>;rel=modulepreload;as=script"
            }
        })
        return true
    }

Webflux

@Component
class EntryPointFilter: WebFilter {
    override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> {
        val path = exchange.request.uri.path
        val (entry, links) = when (path) {
            "/" -> "/index.html" to listOf(
                    "_assets/style.5676cd4c.css",
                    "_assets/index.bfb29383.js"
            )
            "/env" -> "/env.html" to listOf(
                    "_assets/style.5676cd4c.css",
                    "_assets/TestEnv.675482fe.js",
                    "_assets/index.bfb29383.js"
            )
            "/custom-blocks" -> "/custom-blocks.html" to listOf(
                    "_assets/style.5676cd4c.css",
                    "_assets/One.ce379106.js",
                    "_assets/Two.1f6c8df8.js",
                    "_assets/TestCustomBlocks.4c398f20.js",
                    "_assets/index.bfb29383.js"
            )
            "/dynamic-import.html" -> "/dynamic-import.html" to listOf(
                    "_assets/style.5676cd4c.css",
                    "_assets/TestDynamicImport.12f7cca2.js",
                    "_assets/index.bfb29383.js"
            )
            else -> null to emptyList<String>()
        }
        return if (entry != null) {
            exchange.response.headers.set(
                    "Link",
                    links.joinToString(", ") {
                        when {
                            it.endsWith(".css") -> "</$it>;rel=preload;as=style"
                            /* assume there is only css and js */
                            else -> "</$it>;rel=modulepreload;as=script"
                        }
                    }
            )
            chain.filter(
                    exchange.mutate().request(
                            exchange.request.mutate().path(entry).build()
                    ).build()
            )
        } else {
            chain.filter(exchange)
        }
    }
}

Comment From: bclozel

I'm glad you got your use case solved using another solution.

We'll keep this issue open until we hear from the reactor netty team about actual server push.

Comment From: userquin

I have closed it on netty, must I reopen it?

Comment From: userquin

@bclozel see this on RFC Server Push (HTTP/2)

It says that servers can initiate a push if Link header is present on response and nopush is NOT present, so my approach is not 100% compliant with server push.

The server MAY initiate server push for preload link resources defined by the application for which it is authoritative. 
Initiating server push eliminates the request roundtrip between client and server for the declared preload link resource. 
Optionally, if the use of server push is not desired for a resource declared via the Link header field ([RFC5988]), 
the developer MAY provide an opt-out signal to the server via the nopush target attribute ([RFC5988] section 5.4).

Apache is compliant (and most CDNs), so when I add the Link header to the response, it makes a server push, so you can see a real server push:

apache-link-header

On the other hand, Tomcat and Netty are not compliant (you can see the initiator header is not a server push):

netty-link-header

netty-index-response-headers

Comment From: bclozel

Reactor Netty doesn't support this feature and to be fair, Server Push seems to be abandoned these days: many CDNs don't support it, browsers are removing support, it's been cut from the spec.

These days, Early Hints seems to be favored by implementors.

I'm declining this issue as a result.