I'm trying to create a WebClient configuration that can retry requests on failure, with a delay. Here's the class, and the unit test is written below but the requests aren't being retried.


class RetryableWebClientConfig  {

    override fun buildWebclient(baseUri: String): WebClient {
        val newWebClient = webClient.mutate().filter { clientRequest: ClientRequest,
                                                       next: ExchangeFunction ->
            next.exchange(clientRequest)
                .retryWhen(retryStrategy())
        }.build()

        return newWebClient;
    }

    fun retryStrategy(): RetryBackoffSpec {
        val retry = Retry.backoff(3, Duration.ofMillis(100))
            .filter { throwable ->
                throwable is WebClientResponseException.InternalServerError
            }
            .onRetryExhaustedThrow { retryBackoffSpec, retrySignal -> retrySignal.failure() }
        return retry
    }
}

This is a WebClient configuration file that I have created to use retry globally in the ApplicationContext.


@SpringBootTest
@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_CLASS)
class RetryableWebClientConfigTest {

    private val logger = KotlinLogging.logger {}

    private val mockPort = getFreePort()


    private val clientURL = "http://localhost:$mockPort"


    private val wireMockServer = WireMockServer(WireMockConfiguration.options().port(mockPort))


    private fun getFreePort(): Int {
        return try {
            ServerSocket(0).use { it.localPort }
        } catch (e: IOException) {
            throw RuntimeException(e)
        }
    }

    @BeforeEach
    fun setUp() {
        wireMockServer.start()
        WireMock.configureFor("localhost", mockPort)

        wireMockServer.addStubMapping(StubMapping(RequestPattern.everything(), ResponseDefinition(500, "Error")))

        wireMockServer.addMockServiceRequestListener { request, response -> logger.info("Response", response)  }
    }

    @Test
    @DirtiesContext
    fun shouldRetryOnFailure() {

        val webClient = RetryableWebClientConfig().buildWebclient("$clientURL/123")
        try {
            val response = webClient.get().retrieve().bodyToMono(String::class.java).block()
        } catch (e: Exception) {
            logger.info { e }
        }

        println("COUNT ${wireMockServer.countRequestsMatching(RequestPattern.everything()).count}")
        assert(wireMockServer.countRequestsMatching(RequestPattern.everything()).count == 3)
    }
}

Comment From: rstoyanchev

Thanks for getting in touch, but it feels like this is a question that would be better suited to Stack Overflow. As mentioned in the guidelines for contributing, we prefer to use the issue tracker only for bugs and enhancements. Feel free to update this issue with a link to the re-posted question (so that other people can find it) or add some more details if you feel this is a genuine bug.

As an additional comment, retry is triggered for onError, but the exchange method sees onNext with a ClientResponse, so you'll need to handle that via flatMap and only then can retry.