Closed as duplicate
Description
Affects: 3.2.2
It appears when custom coroutine contexts are applied prior to entering a WebClient filter, the custom contexts are no available inside the filter.
When running the below application we add a custom context in the controller which is found in the coroutine context. However when we attempt to fetch the custom context in the filter, it comes back null. You can see this by running the app and hitting
GET http://localhost:8080/test
package com.target.test
import io.netty.channel.ChannelOption
import io.netty.handler.timeout.WriteTimeoutHandler
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.reactive.awaitFirstOrNull
import kotlinx.coroutines.withContext
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.ComponentScan
import org.springframework.context.annotation.Configuration
import org.springframework.http.ResponseEntity
import org.springframework.http.client.reactive.ReactorClientHttpConnector
import org.springframework.stereotype.Component
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.reactive.function.client.*
import reactor.netty.http.client.HttpClient
import java.net.URI
import java.time.Duration
import java.util.concurrent.TimeUnit
import kotlin.coroutines.AbstractCoroutineContextElement
import kotlin.coroutines.CoroutineContext
@SpringBootApplication
@ComponentScan(basePackages = ["com.target"])
class TestApplication
fun main(args: Array<String>) {
runApplication<TestApplication>(*args)
}
@Configuration
class WebClientConfig {
@Bean
@Suppress("unused")
fun webClient(
filterMissingCustomCoroutineContext: FilterMissingCustomCoroutineContext
): WebClient {
val client = WebClient.builder()
.clientConnector(ReactorClientHttpConnector(httpClient()))
.filter(filterMissingCustomCoroutineContext)
.build()
return client
}
private fun httpClient(): HttpClient {
return HttpClient.create()
.responseTimeout(Duration.ofMillis(10000))
.doOnConnected {
it.addHandlerFirst(WriteTimeoutHandler(10000, TimeUnit.MILLISECONDS))
}
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000)
}
}
@Component
class FilterMissingCustomCoroutineContext: CoExchangeFilterFunction() {
override suspend fun filter(request: ClientRequest, next: CoExchangeFunction): ClientResponse {
val customContext = currentCoroutineContext()[CustomCoroutineContext]
println("In client filter, custom context is $customContext")
try {
assert(customContext != null)
}
catch(t: Throwable) {
t.printStackTrace()
throw t
}
return next.exchange(request)
}
}
@RestController
class TestController @Autowired constructor(
private val webClient: WebClient
) {
@GetMapping("/test")
suspend fun test(): ResponseEntity<String>? {
return withContext(CustomCoroutineContext("test")) {
val customContext = currentCoroutineContext()[CustomCoroutineContext]
assert(customContext != null)
println("In controller, custom context is $customContext")
webClient.get()
.uri(URI("https://github.com/spring-projects/spring-framework/issues/26977"))
.retrieve()
.toEntity(String::class.java)
.awaitFirstOrNull()
}
}
}
data class CustomCoroutineContext(val value: String) :
AbstractCoroutineContextElement(Key) {
companion object Key : CoroutineContext.Key<CustomCoroutineContext>
}