Repository: ktorio/ktor
File: ktor-client/ktor-client-core/common/src/io/ktor/client/plugins/HttpSend.kt (Kotlin)

Code before:
     * A plugin's installation object
     *
     * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.client.plugins.HttpSend.Plugin)
     */
    public companion object Plugin : HttpClientPlugin<Config, HttpSend> {
        override val key: AttributeKey<HttpSend> = AttributeKey("HttpSend")

        override fun prepare(block: Config.() -> Unit): HttpSend {
            val config = Config().apply(block)
            return HttpSend(config.maxSendCount)
        }

        override fun install(plugin: HttpSend, scope: HttpClient) {
            // default send scenario
            scope.requestPipeline.intercept(HttpRequestPipeline.Send) { content ->
                check(content is OutgoingContent) {
                    """
|Fail to prepare request body for sending. 
|The body type is: ${content::class}, with Content-Type: ${context.contentType()}.
|
|If you expect serialized body, please check that you have installed the corresponding plugin(like `ContentNegotiation`) and set `Content-Type` header."""
                        .trimMargin()
                }
                context.setBody(content)
                val maxRetries = context.attributes.getOrNull(MaxRetriesPerRequestAttributeKey)
                val maxSendCount = if (maxRetries != null && maxRetries > plugin.maxSendCount) {
                    if (maxRetries < Int.MAX_VALUE) {
                        maxRetries + 1 // +1 for the initial request
                    } else {
                        maxRetries
                    }
                } else {
                    plugin.maxSendCount
                }
                val realSender: Sender = DefaultSender(maxSendCount, scope)
                var interceptedSender = realSender
                for (interceptor in plugin.interceptors.reversed()) {
                    interceptedSender = InterceptedSender(interceptor, interceptedSender)
                }
                val call = interceptedSender.execute(context)
                proceedWith(call)
            }
        }
    }

    private class InterceptedSender(
        private val interceptor: HttpSendInterceptor,
        private val nextSender: Sender
    ) : Sender {

        override suspend fun execute(requestBuilder: HttpRequestBuilder): HttpClientCall {

Review comment (suggestion):
Because maxSendCount is the total number of sends while maxRetries counts retries only, the condition maxRetries > plugin.maxSendCount leaves maxSendCount unchanged when maxRetries == plugin.maxSendCount, which still permits only maxSendCount - 1 retries. In that case the last retry attempt will throw SendCountExceedException (e.g., maxRetries=20 with the default maxSendCount=20 allows only 19 retries); this should compare against maxRetries + 1 or use >=.

Code after:
     * A plugin's installation object
     *
     * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.client.plugins.HttpSend.Plugin)
     */
    public companion object Plugin : HttpClientPlugin<Config, HttpSend> {
        override val key: AttributeKey<HttpSend> = AttributeKey("HttpSend")

        override fun prepare(block: Config.() -> Unit): HttpSend {
            val config = Config().apply(block)
            return HttpSend(config.maxSendCount)
        }

        override fun install(plugin: HttpSend, scope: HttpClient) {
            // default send scenario
            scope.requestPipeline.intercept(HttpRequestPipeline.Send) { content ->
                check(content is OutgoingContent) {
                    """
|Fail to prepare request body for sending. 
|The body type is: ${content::class}, with Content-Type: ${context.contentType()}.
|
|If you expect serialized body, please check that you have installed the corresponding plugin(like `ContentNegotiation`) and set `Content-Type` header."""
                        .trimMargin()
                }
                context.setBody(content)
                val maxRetries = context.attributes.getOrNull(MaxRetriesPerRequestAttributeKey)
                val maxSendCount = if (maxRetries != null && maxRetries >= plugin.maxSendCount) {
                    if (maxRetries < Int.MAX_VALUE) {
                        maxRetries + 1 // +1 for the initial request
                    } else {
                        maxRetries
                    }
                } else {
                    plugin.maxSendCount
                }
                val realSender: Sender = DefaultSender(maxSendCount, scope)
                var interceptedSender = realSender
                for (interceptor in plugin.interceptors.reversed()) {
                    interceptedSender = InterceptedSender(interceptor, interceptedSender)
                }
                val call = interceptedSender.execute(context)
                proceedWith(call)
            }
        }
    }

    private class InterceptedSender(
        private val interceptor: HttpSendInterceptor,
        private val nextSender: Sender
    ) : Sender {

        override suspend fun execute(requestBuilder: HttpRequestBuilder): HttpClientCall {