diff --git a/build.gradle.kts b/build.gradle.kts index 8b53b2a42..0cf3aee3c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -26,6 +26,7 @@ dependencies { api("com.google.code.gson:gson:2.10") api("org.apache.httpcomponents:httpclient:4.5.13") + api("org.apache.httpcomponents.client5:httpclient5:5.5") api("io.prometheus:prometheus-metrics-core:1.1.0") testImplementation(kotlin("test")) diff --git a/src/main/kotlin/com/ecwid/apiclient/v3/ApiClient.kt b/src/main/kotlin/com/ecwid/apiclient/v3/ApiClient.kt index 4968d6e91..b2dc63c14 100644 --- a/src/main/kotlin/com/ecwid/apiclient/v3/ApiClient.kt +++ b/src/main/kotlin/com/ecwid/apiclient/v3/ApiClient.kt @@ -50,6 +50,7 @@ import com.ecwid.apiclient.v3.dto.variation.result.* import com.ecwid.apiclient.v3.httptransport.HttpTransport import com.ecwid.apiclient.v3.impl.* import com.ecwid.apiclient.v3.jsontransformer.JsonTransformerProvider +import java.io.Closeable import kotlin.reflect.KClass open class ApiClient private constructor( @@ -98,7 +99,8 @@ open class ApiClient private constructor( SlugInfoApiClient by slugInfoApiClient, ProductReviewsApiClient by productReviewsApiClient, StoreExtrafieldsApiClient by storeExtrafieldsApiClient, - SwatchesApiClient by swatchesApiClient { + SwatchesApiClient by swatchesApiClient, + Closeable { constructor(apiClientHelper: ApiClientHelper) : this( apiClientHelper = apiClientHelper, @@ -126,6 +128,10 @@ open class ApiClient private constructor( swatchesApiClient = SwatchesApiClientImpl(apiClientHelper), ) + override fun close() { + apiClientHelper.httpTransport.close() + } + companion object { fun create( diff --git a/src/main/kotlin/com/ecwid/apiclient/v3/httptransport/HttpTransport.kt b/src/main/kotlin/com/ecwid/apiclient/v3/httptransport/HttpTransport.kt index 239c1ec2d..dc28f7e21 100644 --- a/src/main/kotlin/com/ecwid/apiclient/v3/httptransport/HttpTransport.kt +++ b/src/main/kotlin/com/ecwid/apiclient/v3/httptransport/HttpTransport.kt @@ -1,5 +1,7 @@ package com.ecwid.apiclient.v3.httptransport -interface HttpTransport { +import java.io.Closeable + +interface HttpTransport : Closeable { fun makeHttpRequest(httpRequest: HttpRequest): HttpResponse } diff --git a/src/main/kotlin/com/ecwid/apiclient/v3/httptransport/impl/ApacheCommonsHttpClientTransport.kt b/src/main/kotlin/com/ecwid/apiclient/v3/httptransport/impl/ApacheCommonsHttpClientTransport.kt index c07db3fd0..7a4711d65 100644 --- a/src/main/kotlin/com/ecwid/apiclient/v3/httptransport/impl/ApacheCommonsHttpClientTransport.kt +++ b/src/main/kotlin/com/ecwid/apiclient/v3/httptransport/impl/ApacheCommonsHttpClientTransport.kt @@ -8,6 +8,7 @@ import org.apache.http.client.HttpClient import org.apache.http.client.config.RequestConfig import org.apache.http.impl.client.HttpClientBuilder import org.apache.http.impl.conn.PoolingHttpClientConnectionManager +import java.io.Closeable import java.io.IOException private const val DEFAULT_CONNECTION_TIMEOUT = 10_000 // 10 sec @@ -33,6 +34,7 @@ internal const val MAX_RATE_LIMIT_RETRY_INTERVAL_SECONDS = 60L val EMPTY_WAITING_REACTION: (Long) -> Unit = { } val EMPTY_BEFORE_REQUEST_ACTION: () -> Unit = { } +@Deprecated("Use ApacheCommonsHttpClientTransport from client5 package") open class ApacheCommonsHttpClientTransport( private val httpClient: HttpClient = buildHttpClient(), private val rateLimitRetryStrategy: RateLimitRetryStrategy = SleepForRetryAfterRateLimitRetryStrategy(), @@ -106,6 +108,12 @@ open class ApacheCommonsHttpClientTransport( } } + override fun close() { + if (httpClient is Closeable) { + httpClient.close() + } + } + companion object { private fun buildHttpClient( diff --git a/src/main/kotlin/com/ecwid/apiclient/v3/httptransport/impl/client5/ApacheCommonsHttpClient5Transport.kt b/src/main/kotlin/com/ecwid/apiclient/v3/httptransport/impl/client5/ApacheCommonsHttpClient5Transport.kt new file mode 100644 index 000000000..dc142c64c --- /dev/null +++ b/src/main/kotlin/com/ecwid/apiclient/v3/httptransport/impl/client5/ApacheCommonsHttpClient5Transport.kt @@ -0,0 +1,154 @@ +package com.ecwid.apiclient.v3.httptransport.impl.client5 + +import com.ecwid.apiclient.v3.httptransport.HttpRequest +import com.ecwid.apiclient.v3.httptransport.HttpResponse +import com.ecwid.apiclient.v3.httptransport.HttpTransport +import org.apache.hc.client5.http.classic.HttpClient +import org.apache.hc.client5.http.config.ConnectionConfig +import org.apache.hc.client5.http.config.RequestConfig +import org.apache.hc.client5.http.impl.classic.HttpClients +import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder +import org.apache.hc.core5.http.Header +import java.io.Closeable +import java.io.IOException +import java.util.concurrent.TimeUnit + +private const val DEFAULT_CONNECTION_TIMEOUT = 10_000L // 10 sec +private const val DEFAULT_READ_TIMEOUT = 60_000 // 1 min + +private const val DEFAULT_MAX_CONNECTIONS = 10 + +/** + * Number of attempts to retry request if server responded with 429 + */ +internal const val DEFAULT_RATE_LIMIT_ATTEMPTS = 2 + +/** + * Number of seconds to wait until next attempt, if server didn't send Retry-After header + */ +internal const val DEFAULT_RATE_LIMIT_RETRY_INTERVAL_SECONDS = 10L + +/** + * Maximal delay in seconds before next attempt + */ +internal const val MAX_RATE_LIMIT_RETRY_INTERVAL_SECONDS = 60L + +val EMPTY_WAITING_REACTION: (Long) -> Unit = { } +val EMPTY_BEFORE_REQUEST_ACTION: () -> Unit = { } + +open class ApacheCommonsHttpClient5Transport( + private val httpClient: HttpClient = buildHttpClient(), + private val rateLimitRetryStrategy: RateLimitRetryStrategy = SleepForRetryAfterRateLimitRetryStrategy(), +) : HttpTransport { + + constructor( + httpClient: HttpClient, + defaultRateLimitAttempts: Int = DEFAULT_RATE_LIMIT_ATTEMPTS, + defaultRateLimitRetryInterval: Long = DEFAULT_RATE_LIMIT_RETRY_INTERVAL_SECONDS, + maxRateLimitRetryInterval: Long = MAX_RATE_LIMIT_RETRY_INTERVAL_SECONDS, + onEverySecondOfWaiting: (Long) -> Unit = EMPTY_WAITING_REACTION, + beforeEachRequestAttempt: () -> Unit = EMPTY_BEFORE_REQUEST_ACTION, + ) : this( + httpClient, + rateLimitRetryStrategy = SleepForRetryAfterRateLimitRetryStrategy( + defaultRateLimitAttempts = defaultRateLimitAttempts, + defaultRateLimitRetryInterval = defaultRateLimitRetryInterval, + maxRateLimitRetryInterval = maxRateLimitRetryInterval, + onEverySecondOfWaiting = onEverySecondOfWaiting, + beforeEachRequestAttempt = beforeEachRequestAttempt + ) + ) + + constructor( + defaultConnectionTimeout: Long = DEFAULT_CONNECTION_TIMEOUT, + defaultReadTimeout: Int = DEFAULT_READ_TIMEOUT, + defaultMaxConnections: Int = DEFAULT_MAX_CONNECTIONS, + defaultHeaders: List
= emptyList(), + rateLimitRetryStrategy: RateLimitRetryStrategy + ) : this( + httpClient = buildHttpClient( + defaultConnectionTimeout = defaultConnectionTimeout, + defaultReadTimeout = defaultReadTimeout, + defaultMaxConnections = defaultMaxConnections, + defaultHeaders = defaultHeaders + ), + rateLimitRetryStrategy = rateLimitRetryStrategy + ) + + constructor( + defaultConnectionTimeout: Long = DEFAULT_CONNECTION_TIMEOUT, + defaultReadTimeout: Int = DEFAULT_READ_TIMEOUT, + defaultMaxConnections: Int = DEFAULT_MAX_CONNECTIONS, + defaultRateLimitAttempts: Int = DEFAULT_RATE_LIMIT_ATTEMPTS, + defaultRateLimitRetryInterval: Long = DEFAULT_RATE_LIMIT_RETRY_INTERVAL_SECONDS, + maxRateLimitRetryInterval: Long = MAX_RATE_LIMIT_RETRY_INTERVAL_SECONDS, + defaultHeaders: List
= emptyList(), + onEverySecondOfWaiting: (Long) -> Unit = EMPTY_WAITING_REACTION, + beforeEachRequestAttempt: () -> Unit = EMPTY_BEFORE_REQUEST_ACTION, + ) : this( + httpClient = buildHttpClient( + defaultConnectionTimeout = defaultConnectionTimeout, + defaultReadTimeout = defaultReadTimeout, + defaultMaxConnections = defaultMaxConnections, + defaultHeaders = defaultHeaders + ), + rateLimitRetryStrategy = SleepForRetryAfterRateLimitRetryStrategy( + defaultRateLimitAttempts = defaultRateLimitAttempts, + defaultRateLimitRetryInterval = defaultRateLimitRetryInterval, + maxRateLimitRetryInterval = maxRateLimitRetryInterval, + onEverySecondOfWaiting = onEverySecondOfWaiting, + beforeEachRequestAttempt = beforeEachRequestAttempt, + ) + ) + + override fun makeHttpRequest(httpRequest: HttpRequest): HttpResponse { + return try { + rateLimitRetryStrategy.makeHttpRequest(httpClient, httpRequest) + } catch (e: IOException) { + HttpResponse.TransportError(e) + } + } + + override fun close() { + if (httpClient is Closeable) { + httpClient.close() + } + } + + companion object { + + private fun buildHttpClient( + defaultConnectionTimeout: Long = DEFAULT_CONNECTION_TIMEOUT, + defaultReadTimeout: Int = DEFAULT_READ_TIMEOUT, + defaultMaxConnections: Int = DEFAULT_MAX_CONNECTIONS, + defaultHeaders: List
= emptyList(), + ): HttpClient { + val connectionConfig = ConnectionConfig.custom() + .setConnectTimeout(defaultConnectionTimeout, TimeUnit.SECONDS) + .setSocketTimeout(defaultReadTimeout, TimeUnit.SECONDS) + .build() + + val connectionManager = PoolingHttpClientConnectionManagerBuilder.create() + .setMaxConnTotal(defaultMaxConnections) + .setMaxConnPerRoute(defaultMaxConnections) + .setDefaultConnectionConfig(connectionConfig) + .build() + + val requestConfig = RequestConfig.custom() + .setConnectionRequestTimeout(defaultConnectionTimeout, TimeUnit.SECONDS) + .build() + + val httpClientBuilder = HttpClients.custom() + .setConnectionManager(connectionManager) + .setDefaultRequestConfig(requestConfig) + .setRedirectStrategy(RemoveDisallowedHeadersRedirectStrategy()) + // TODO .setRetryHandler() + // TODO .setServiceUnavailableRetryStrategy() + if (defaultHeaders.isNotEmpty()) { + httpClientBuilder.setDefaultHeaders(defaultHeaders) + } + return httpClientBuilder.build() + } + } +} + diff --git a/src/main/kotlin/com/ecwid/apiclient/v3/httptransport/impl/client5/ConvertUtils.kt b/src/main/kotlin/com/ecwid/apiclient/v3/httptransport/impl/client5/ConvertUtils.kt new file mode 100644 index 000000000..bffcee453 --- /dev/null +++ b/src/main/kotlin/com/ecwid/apiclient/v3/httptransport/impl/client5/ConvertUtils.kt @@ -0,0 +1,63 @@ +package com.ecwid.apiclient.v3.httptransport.impl.client5 + +import com.ecwid.apiclient.v3.httptransport.HttpRequest +import com.ecwid.apiclient.v3.httptransport.HttpResponse +import com.ecwid.apiclient.v3.httptransport.TransportHttpBody +import org.apache.hc.core5.http.ClassicHttpRequest +import org.apache.hc.core5.http.ClassicHttpResponse +import org.apache.hc.core5.http.ContentType +import org.apache.hc.core5.http.HttpEntity +import org.apache.hc.core5.http.HttpStatus +import org.apache.hc.core5.http.io.entity.* +import org.apache.hc.core5.http.io.support.ClassicRequestBuilder + +internal fun HttpRequest.toHttpUriRequest(): ClassicHttpRequest { + val requestBuilder = when (this) { + is HttpRequest.HttpGetRequest -> { + ClassicRequestBuilder.get(uri) + } + is HttpRequest.HttpPostRequest -> { + ClassicRequestBuilder + .post(uri) + .setEntity(transportHttpBody.toEntity()) + } + is HttpRequest.HttpPutRequest -> { + ClassicRequestBuilder + .put(uri) + .setEntity(transportHttpBody.toEntity()) + } + is HttpRequest.HttpDeleteRequest -> { + ClassicRequestBuilder.delete(uri) + } + } + + return requestBuilder + .apply updated@{ + this@updated.charset = Charsets.UTF_8 + this@toHttpUriRequest.params.forEach(this@updated::addParameter) + this@toHttpUriRequest.headers.forEach(this@updated::addHeader) + } + .build() +} + +internal fun ClassicHttpResponse.toApiResponse(): HttpResponse { + val responseBytes = EntityUtils.toByteArray(entity) + return if (code == HttpStatus.SC_OK) { + HttpResponse.Success(responseBytes) + } else { + HttpResponse.Error(code, reasonPhrase, responseBytes) + } +} + +private fun TransportHttpBody.toEntity(): HttpEntity? = when (this) { + is TransportHttpBody.EmptyBody -> + null + is TransportHttpBody.InputStreamBody -> + BufferedHttpEntity(InputStreamEntity(stream, mimeType.toContentType())) + is TransportHttpBody.ByteArrayBody -> + ByteArrayEntity(byteArray, mimeType.toContentType()) + is TransportHttpBody.LocalFileBody -> + FileEntity(file, mimeType.toContentType()) +} + +private fun String.toContentType(): ContentType = ContentType.create(this, Charsets.UTF_8) diff --git a/src/main/kotlin/com/ecwid/apiclient/v3/httptransport/impl/client5/NoRetryRateLimitRetryStrategy.kt b/src/main/kotlin/com/ecwid/apiclient/v3/httptransport/impl/client5/NoRetryRateLimitRetryStrategy.kt new file mode 100644 index 000000000..883e259cf --- /dev/null +++ b/src/main/kotlin/com/ecwid/apiclient/v3/httptransport/impl/client5/NoRetryRateLimitRetryStrategy.kt @@ -0,0 +1,13 @@ +package com.ecwid.apiclient.v3.httptransport.impl.client5 + +import com.ecwid.apiclient.v3.httptransport.HttpRequest +import com.ecwid.apiclient.v3.httptransport.HttpResponse +import org.apache.hc.client5.http.classic.HttpClient + +class NoRetryRateLimitRetryStrategy : RateLimitRetryStrategy { + override fun makeHttpRequest(httpClient: HttpClient, httpRequest: HttpRequest): HttpResponse { + return httpClient.execute(httpRequest.toHttpUriRequest()) { response -> + response.toApiResponse() + } + } +} diff --git a/src/main/kotlin/com/ecwid/apiclient/v3/httptransport/impl/client5/RateLimitRetryStrategy.kt b/src/main/kotlin/com/ecwid/apiclient/v3/httptransport/impl/client5/RateLimitRetryStrategy.kt new file mode 100644 index 000000000..ed4820f10 --- /dev/null +++ b/src/main/kotlin/com/ecwid/apiclient/v3/httptransport/impl/client5/RateLimitRetryStrategy.kt @@ -0,0 +1,10 @@ +package com.ecwid.apiclient.v3.httptransport.impl.client5 + +import com.ecwid.apiclient.v3.httptransport.HttpRequest +import com.ecwid.apiclient.v3.httptransport.HttpResponse +import org.apache.hc.client5.http.classic.HttpClient + +interface RateLimitRetryStrategy { + fun makeHttpRequest(httpClient: HttpClient, httpRequest: HttpRequest): HttpResponse +} + diff --git a/src/main/kotlin/com/ecwid/apiclient/v3/httptransport/impl/client5/RateLimitedHttpClientWrapper.kt b/src/main/kotlin/com/ecwid/apiclient/v3/httptransport/impl/client5/RateLimitedHttpClientWrapper.kt new file mode 100644 index 000000000..54b28e503 --- /dev/null +++ b/src/main/kotlin/com/ecwid/apiclient/v3/httptransport/impl/client5/RateLimitedHttpClientWrapper.kt @@ -0,0 +1,89 @@ +package com.ecwid.apiclient.v3.httptransport.impl.client5 + +import com.ecwid.apiclient.v3.metric.RequestRetrySleepMetric +import org.apache.hc.client5.http.classic.HttpClient +import org.apache.hc.core5.http.ClassicHttpRequest +import org.apache.hc.core5.http.ClassicHttpResponse +import org.apache.hc.core5.http.io.HttpClientResponseHandler +import org.apache.hc.core5.http.io.entity.EntityUtils +import java.io.IOException +import java.util.concurrent.TimeUnit +import java.util.logging.Logger + +private val log = Logger.getLogger(RateLimitedHttpClientWrapper::class.qualifiedName) + +private const val SC_TOO_MANY_REQUESTS = 429 + +/** + * Wrapper for httpClient, which retries requests if faces 429 error response. + * Respects server's Retry-After header if provided. + */ +open class RateLimitedHttpClientWrapper( + private val httpClient: HttpClient, + private val defaultRateLimitRetryInterval: Long = DEFAULT_RATE_LIMIT_RETRY_INTERVAL_SECONDS, + private val maxRateLimitRetryInterval: Long = MAX_RATE_LIMIT_RETRY_INTERVAL_SECONDS, + private val totalAttempts: Int = DEFAULT_RATE_LIMIT_ATTEMPTS, + private val onEverySecondOfWaiting: (Long) -> Unit = { }, + private val beforeEachRequestAttempt: () -> Unit = { }, +) { + + @Throws(IOException::class) + fun execute(request: ClassicHttpRequest, responseHandler: HttpClientResponseHandler): T { + return executeWithRetry(request, totalAttempts, responseHandler) + } + + private fun executeWithRetry( + request: ClassicHttpRequest, + attemptsLeft: Int, + responseHandler: HttpClientResponseHandler + ): T { + beforeEachRequestAttempt() + return httpClient.execute(request) { response -> + if (response.code == SC_TOO_MANY_REQUESTS && attemptsLeft > 0) { + process429( + request, + response, + attemptsLeft - 1, // decrement attempts reminder + responseHandler + ) + } else if (response.code == SC_TOO_MANY_REQUESTS && attemptsLeft <= 0) { + log.warning("Request ${request.uri.path} rate-limited: no more attempts.") + responseHandler.handleResponse(response) + } else { + responseHandler.handleResponse(response) + } + } + } + + private fun process429( + request: ClassicHttpRequest, + response: ClassicHttpResponse, + attemptsLeft: Int, + responseHandler: HttpClientResponseHandler + ): T { + // server must inform how long to wait + val waitInterval = response.getFirstHeader("Retry-After")?.value?.toLong() + ?: defaultRateLimitRetryInterval + return if (waitInterval <= maxRateLimitRetryInterval) { + // return used http connection to pool before retry + EntityUtils.consume(response.entity) + // if server requested acceptable time, we'll wait + log.info("Request ${request.uri.path} rate-limited: waiting $waitInterval seconds...") + waitSeconds(waitInterval, onEverySecondOfWaiting) + log.info("Retrying ${request.uri.path} after $waitInterval-s pause...") + executeWithRetry(request, attemptsLeft, responseHandler) + } else { + // too long to wait - let's return the original error + log.warning("Request ${request.uri.path} rate-limited: too long to wait ($waitInterval).") + responseHandler.handleResponse(response) + } + } + + private fun waitSeconds(waitInterval: Long, onEverySecondOfWaiting: (Long) -> Unit) { + for (second in 1..waitInterval) { + TimeUnit.SECONDS.sleep(1) + onEverySecondOfWaiting(second) + RequestRetrySleepMetric.inc() + } + } +} diff --git a/src/main/kotlin/com/ecwid/apiclient/v3/httptransport/impl/client5/RemoveDisallowedHeadersRedirectStrategy.kt b/src/main/kotlin/com/ecwid/apiclient/v3/httptransport/impl/client5/RemoveDisallowedHeadersRedirectStrategy.kt new file mode 100644 index 000000000..ba6580fef --- /dev/null +++ b/src/main/kotlin/com/ecwid/apiclient/v3/httptransport/impl/client5/RemoveDisallowedHeadersRedirectStrategy.kt @@ -0,0 +1,54 @@ +package com.ecwid.apiclient.v3.httptransport.impl.client5 + +import org.apache.hc.client5.http.impl.DefaultRedirectStrategy +import org.apache.hc.core5.http.HttpHost +import org.apache.hc.core5.http.HttpRequest +import org.apache.hc.core5.http.protocol.HttpContext + + +/** + * List of headers that can be passed to a redirect location. + * We should NOT expose any custom headers and Authorization header to external sources + */ +private val allowedHeaders = setOf( + "Accept", + "Accept-Charset", + "Accept-Encoding", + "Accept-Language", + "Access-Control-Request-Method", + "Access-Control-Request-Headers", + "Cache-Control", + "Connection", + "Content-Encoding", + "Content-Length", + "Content-Type", + "Date", + "Host", + "HTTP2-Settings", + "If-Match", + "If-Modified-Since", + "If-None-Match", + "If-Unmodified-Since", + "Origin", + "Referer", + "User-Agent", + "X-Forwarded-For", + "X-Forwarded-Host", + "X-Forwarded-Proto" +) + +class RemoveDisallowedHeadersRedirectStrategy : DefaultRedirectStrategy() { + + @Suppress("SpreadOperator") + override fun isRedirectAllowed( + currentTarget: HttpHost?, + newTarget: HttpHost?, + redirect: HttpRequest?, + context: HttpContext? + ): Boolean { + redirect?.headers?.filter { it.name in allowedHeaders }?.toTypedArray()?.also { + redirect.setHeaders(*it) + } + return true + } +} diff --git a/src/main/kotlin/com/ecwid/apiclient/v3/httptransport/impl/client5/SleepForRetryAfterRateLimitRetryStrategy.kt b/src/main/kotlin/com/ecwid/apiclient/v3/httptransport/impl/client5/SleepForRetryAfterRateLimitRetryStrategy.kt new file mode 100644 index 000000000..26420574e --- /dev/null +++ b/src/main/kotlin/com/ecwid/apiclient/v3/httptransport/impl/client5/SleepForRetryAfterRateLimitRetryStrategy.kt @@ -0,0 +1,49 @@ +package com.ecwid.apiclient.v3.httptransport.impl.client5 + +import com.ecwid.apiclient.v3.httptransport.HttpRequest +import com.ecwid.apiclient.v3.httptransport.HttpResponse +import com.ecwid.apiclient.v3.httptransport.TransportHttpBody +import org.apache.hc.client5.http.classic.HttpClient +import org.apache.hc.core5.http.ClassicHttpRequest +import java.io.IOException + +class SleepForRetryAfterRateLimitRetryStrategy( + private val defaultRateLimitRetryInterval: Long = DEFAULT_RATE_LIMIT_RETRY_INTERVAL_SECONDS, + private val maxRateLimitRetryInterval: Long = MAX_RATE_LIMIT_RETRY_INTERVAL_SECONDS, + private val defaultRateLimitAttempts: Int = DEFAULT_RATE_LIMIT_ATTEMPTS, + private val onEverySecondOfWaiting: (Long) -> Unit = EMPTY_WAITING_REACTION, + private val beforeEachRequestAttempt: () -> Unit = EMPTY_BEFORE_REQUEST_ACTION, +) : RateLimitRetryStrategy { + + override fun makeHttpRequest(httpClient: HttpClient, httpRequest: HttpRequest): HttpResponse { + val request = httpRequest.toHttpUriRequest() + return if (httpRequest.transportHttpBody is TransportHttpBody.InputStreamBody) { + executeWithoutRetry(httpClient, request) + } else { + executeWithRetryOnRateLimited(httpClient, request) + } + } + + private fun executeWithoutRetry(httpClient: HttpClient, request: ClassicHttpRequest) = + httpClient.execute(request) { response -> + response.toApiResponse() + } + + private fun executeWithRetryOnRateLimited(httpClient: HttpClient, request: ClassicHttpRequest): HttpResponse { + return try { + RateLimitedHttpClientWrapper( + httpClient, + defaultRateLimitRetryInterval, + maxRateLimitRetryInterval, + defaultRateLimitAttempts, + onEverySecondOfWaiting, + beforeEachRequestAttempt + ).execute(request) { response -> + response.toApiResponse() + } + } catch (e: IOException) { + HttpResponse.TransportError(e) + } + } + +}