Skip to content

Commit b7053b3

Browse files
Merge pull request #498 from Ecwid/ECWID-165744-httpclient5
ECWID-165744 Update Apache httpclient to httpclient 5.5
2 parents 2b13036 + e728161 commit b7053b3

11 files changed

+451
-2
lines changed

build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ dependencies {
2626

2727
api("com.google.code.gson:gson:2.10")
2828
api("org.apache.httpcomponents:httpclient:4.5.13")
29+
api("org.apache.httpcomponents.client5:httpclient5:5.5")
2930
api("io.prometheus:prometheus-metrics-core:1.1.0")
3031

3132
testImplementation(kotlin("test"))

src/main/kotlin/com/ecwid/apiclient/v3/ApiClient.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ import com.ecwid.apiclient.v3.dto.variation.result.*
5050
import com.ecwid.apiclient.v3.httptransport.HttpTransport
5151
import com.ecwid.apiclient.v3.impl.*
5252
import com.ecwid.apiclient.v3.jsontransformer.JsonTransformerProvider
53+
import java.io.Closeable
5354
import kotlin.reflect.KClass
5455

5556
open class ApiClient private constructor(
@@ -98,7 +99,8 @@ open class ApiClient private constructor(
9899
SlugInfoApiClient by slugInfoApiClient,
99100
ProductReviewsApiClient by productReviewsApiClient,
100101
StoreExtrafieldsApiClient by storeExtrafieldsApiClient,
101-
SwatchesApiClient by swatchesApiClient {
102+
SwatchesApiClient by swatchesApiClient,
103+
Closeable {
102104

103105
constructor(apiClientHelper: ApiClientHelper) : this(
104106
apiClientHelper = apiClientHelper,
@@ -126,6 +128,10 @@ open class ApiClient private constructor(
126128
swatchesApiClient = SwatchesApiClientImpl(apiClientHelper),
127129
)
128130

131+
override fun close() {
132+
apiClientHelper.httpTransport.close()
133+
}
134+
129135
companion object {
130136

131137
fun create(
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.ecwid.apiclient.v3.httptransport
22

3-
interface HttpTransport {
3+
import java.io.Closeable
4+
5+
interface HttpTransport : Closeable {
46
fun makeHttpRequest(httpRequest: HttpRequest): HttpResponse
57
}

src/main/kotlin/com/ecwid/apiclient/v3/httptransport/impl/ApacheCommonsHttpClientTransport.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import org.apache.http.client.HttpClient
88
import org.apache.http.client.config.RequestConfig
99
import org.apache.http.impl.client.HttpClientBuilder
1010
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager
11+
import java.io.Closeable
1112
import java.io.IOException
1213

1314
private const val DEFAULT_CONNECTION_TIMEOUT = 10_000 // 10 sec
@@ -33,6 +34,7 @@ internal const val MAX_RATE_LIMIT_RETRY_INTERVAL_SECONDS = 60L
3334
val EMPTY_WAITING_REACTION: (Long) -> Unit = { }
3435
val EMPTY_BEFORE_REQUEST_ACTION: () -> Unit = { }
3536

37+
@Deprecated("Use ApacheCommonsHttpClientTransport from client5 package")
3638
open class ApacheCommonsHttpClientTransport(
3739
private val httpClient: HttpClient = buildHttpClient(),
3840
private val rateLimitRetryStrategy: RateLimitRetryStrategy = SleepForRetryAfterRateLimitRetryStrategy(),
@@ -106,6 +108,12 @@ open class ApacheCommonsHttpClientTransport(
106108
}
107109
}
108110

111+
override fun close() {
112+
if (httpClient is Closeable) {
113+
httpClient.close()
114+
}
115+
}
116+
109117
companion object {
110118

111119
private fun buildHttpClient(
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
package com.ecwid.apiclient.v3.httptransport.impl.client5
2+
3+
import com.ecwid.apiclient.v3.httptransport.HttpRequest
4+
import com.ecwid.apiclient.v3.httptransport.HttpResponse
5+
import com.ecwid.apiclient.v3.httptransport.HttpTransport
6+
import org.apache.hc.client5.http.classic.HttpClient
7+
import org.apache.hc.client5.http.config.ConnectionConfig
8+
import org.apache.hc.client5.http.config.RequestConfig
9+
import org.apache.hc.client5.http.impl.classic.HttpClients
10+
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder
11+
import org.apache.hc.core5.http.Header
12+
import java.io.Closeable
13+
import java.io.IOException
14+
import java.util.concurrent.TimeUnit
15+
16+
private const val DEFAULT_CONNECTION_TIMEOUT = 10_000L // 10 sec
17+
private const val DEFAULT_READ_TIMEOUT = 60_000 // 1 min
18+
19+
private const val DEFAULT_MAX_CONNECTIONS = 10
20+
21+
/**
22+
* Number of attempts to retry request if server responded with 429
23+
*/
24+
internal const val DEFAULT_RATE_LIMIT_ATTEMPTS = 2
25+
26+
/**
27+
* Number of seconds to wait until next attempt, if server didn't send Retry-After header
28+
*/
29+
internal const val DEFAULT_RATE_LIMIT_RETRY_INTERVAL_SECONDS = 10L
30+
31+
/**
32+
* Maximal delay in seconds before next attempt
33+
*/
34+
internal const val MAX_RATE_LIMIT_RETRY_INTERVAL_SECONDS = 60L
35+
36+
val EMPTY_WAITING_REACTION: (Long) -> Unit = { }
37+
val EMPTY_BEFORE_REQUEST_ACTION: () -> Unit = { }
38+
39+
open class ApacheCommonsHttpClient5Transport(
40+
private val httpClient: HttpClient = buildHttpClient(),
41+
private val rateLimitRetryStrategy: RateLimitRetryStrategy = SleepForRetryAfterRateLimitRetryStrategy(),
42+
) : HttpTransport {
43+
44+
constructor(
45+
httpClient: HttpClient,
46+
defaultRateLimitAttempts: Int = DEFAULT_RATE_LIMIT_ATTEMPTS,
47+
defaultRateLimitRetryInterval: Long = DEFAULT_RATE_LIMIT_RETRY_INTERVAL_SECONDS,
48+
maxRateLimitRetryInterval: Long = MAX_RATE_LIMIT_RETRY_INTERVAL_SECONDS,
49+
onEverySecondOfWaiting: (Long) -> Unit = EMPTY_WAITING_REACTION,
50+
beforeEachRequestAttempt: () -> Unit = EMPTY_BEFORE_REQUEST_ACTION,
51+
) : this(
52+
httpClient,
53+
rateLimitRetryStrategy = SleepForRetryAfterRateLimitRetryStrategy(
54+
defaultRateLimitAttempts = defaultRateLimitAttempts,
55+
defaultRateLimitRetryInterval = defaultRateLimitRetryInterval,
56+
maxRateLimitRetryInterval = maxRateLimitRetryInterval,
57+
onEverySecondOfWaiting = onEverySecondOfWaiting,
58+
beforeEachRequestAttempt = beforeEachRequestAttempt
59+
)
60+
)
61+
62+
constructor(
63+
defaultConnectionTimeout: Long = DEFAULT_CONNECTION_TIMEOUT,
64+
defaultReadTimeout: Int = DEFAULT_READ_TIMEOUT,
65+
defaultMaxConnections: Int = DEFAULT_MAX_CONNECTIONS,
66+
defaultHeaders: List<Header> = emptyList(),
67+
rateLimitRetryStrategy: RateLimitRetryStrategy
68+
) : this(
69+
httpClient = buildHttpClient(
70+
defaultConnectionTimeout = defaultConnectionTimeout,
71+
defaultReadTimeout = defaultReadTimeout,
72+
defaultMaxConnections = defaultMaxConnections,
73+
defaultHeaders = defaultHeaders
74+
),
75+
rateLimitRetryStrategy = rateLimitRetryStrategy
76+
)
77+
78+
constructor(
79+
defaultConnectionTimeout: Long = DEFAULT_CONNECTION_TIMEOUT,
80+
defaultReadTimeout: Int = DEFAULT_READ_TIMEOUT,
81+
defaultMaxConnections: Int = DEFAULT_MAX_CONNECTIONS,
82+
defaultRateLimitAttempts: Int = DEFAULT_RATE_LIMIT_ATTEMPTS,
83+
defaultRateLimitRetryInterval: Long = DEFAULT_RATE_LIMIT_RETRY_INTERVAL_SECONDS,
84+
maxRateLimitRetryInterval: Long = MAX_RATE_LIMIT_RETRY_INTERVAL_SECONDS,
85+
defaultHeaders: List<Header> = emptyList(),
86+
onEverySecondOfWaiting: (Long) -> Unit = EMPTY_WAITING_REACTION,
87+
beforeEachRequestAttempt: () -> Unit = EMPTY_BEFORE_REQUEST_ACTION,
88+
) : this(
89+
httpClient = buildHttpClient(
90+
defaultConnectionTimeout = defaultConnectionTimeout,
91+
defaultReadTimeout = defaultReadTimeout,
92+
defaultMaxConnections = defaultMaxConnections,
93+
defaultHeaders = defaultHeaders
94+
),
95+
rateLimitRetryStrategy = SleepForRetryAfterRateLimitRetryStrategy(
96+
defaultRateLimitAttempts = defaultRateLimitAttempts,
97+
defaultRateLimitRetryInterval = defaultRateLimitRetryInterval,
98+
maxRateLimitRetryInterval = maxRateLimitRetryInterval,
99+
onEverySecondOfWaiting = onEverySecondOfWaiting,
100+
beforeEachRequestAttempt = beforeEachRequestAttempt,
101+
)
102+
)
103+
104+
override fun makeHttpRequest(httpRequest: HttpRequest): HttpResponse {
105+
return try {
106+
rateLimitRetryStrategy.makeHttpRequest(httpClient, httpRequest)
107+
} catch (e: IOException) {
108+
HttpResponse.TransportError(e)
109+
}
110+
}
111+
112+
override fun close() {
113+
if (httpClient is Closeable) {
114+
httpClient.close()
115+
}
116+
}
117+
118+
companion object {
119+
120+
private fun buildHttpClient(
121+
defaultConnectionTimeout: Long = DEFAULT_CONNECTION_TIMEOUT,
122+
defaultReadTimeout: Int = DEFAULT_READ_TIMEOUT,
123+
defaultMaxConnections: Int = DEFAULT_MAX_CONNECTIONS,
124+
defaultHeaders: List<Header> = emptyList(),
125+
): HttpClient {
126+
val connectionConfig = ConnectionConfig.custom()
127+
.setConnectTimeout(defaultConnectionTimeout, TimeUnit.SECONDS)
128+
.setSocketTimeout(defaultReadTimeout, TimeUnit.SECONDS)
129+
.build()
130+
131+
val connectionManager = PoolingHttpClientConnectionManagerBuilder.create()
132+
.setMaxConnTotal(defaultMaxConnections)
133+
.setMaxConnPerRoute(defaultMaxConnections)
134+
.setDefaultConnectionConfig(connectionConfig)
135+
.build()
136+
137+
val requestConfig = RequestConfig.custom()
138+
.setConnectionRequestTimeout(defaultConnectionTimeout, TimeUnit.SECONDS)
139+
.build()
140+
141+
val httpClientBuilder = HttpClients.custom()
142+
.setConnectionManager(connectionManager)
143+
.setDefaultRequestConfig(requestConfig)
144+
.setRedirectStrategy(RemoveDisallowedHeadersRedirectStrategy())
145+
// TODO .setRetryHandler()
146+
// TODO .setServiceUnavailableRetryStrategy()
147+
if (defaultHeaders.isNotEmpty()) {
148+
httpClientBuilder.setDefaultHeaders(defaultHeaders)
149+
}
150+
return httpClientBuilder.build()
151+
}
152+
}
153+
}
154+
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package com.ecwid.apiclient.v3.httptransport.impl.client5
2+
3+
import com.ecwid.apiclient.v3.httptransport.HttpRequest
4+
import com.ecwid.apiclient.v3.httptransport.HttpResponse
5+
import com.ecwid.apiclient.v3.httptransport.TransportHttpBody
6+
import org.apache.hc.core5.http.ClassicHttpRequest
7+
import org.apache.hc.core5.http.ClassicHttpResponse
8+
import org.apache.hc.core5.http.ContentType
9+
import org.apache.hc.core5.http.HttpEntity
10+
import org.apache.hc.core5.http.HttpStatus
11+
import org.apache.hc.core5.http.io.entity.*
12+
import org.apache.hc.core5.http.io.support.ClassicRequestBuilder
13+
14+
internal fun HttpRequest.toHttpUriRequest(): ClassicHttpRequest {
15+
val requestBuilder = when (this) {
16+
is HttpRequest.HttpGetRequest -> {
17+
ClassicRequestBuilder.get(uri)
18+
}
19+
is HttpRequest.HttpPostRequest -> {
20+
ClassicRequestBuilder
21+
.post(uri)
22+
.setEntity(transportHttpBody.toEntity())
23+
}
24+
is HttpRequest.HttpPutRequest -> {
25+
ClassicRequestBuilder
26+
.put(uri)
27+
.setEntity(transportHttpBody.toEntity())
28+
}
29+
is HttpRequest.HttpDeleteRequest -> {
30+
ClassicRequestBuilder.delete(uri)
31+
}
32+
}
33+
34+
return requestBuilder
35+
.apply updated@{
36+
this@updated.charset = Charsets.UTF_8
37+
this@toHttpUriRequest.params.forEach(this@updated::addParameter)
38+
this@toHttpUriRequest.headers.forEach(this@updated::addHeader)
39+
}
40+
.build()
41+
}
42+
43+
internal fun ClassicHttpResponse.toApiResponse(): HttpResponse {
44+
val responseBytes = EntityUtils.toByteArray(entity)
45+
return if (code == HttpStatus.SC_OK) {
46+
HttpResponse.Success(responseBytes)
47+
} else {
48+
HttpResponse.Error(code, reasonPhrase, responseBytes)
49+
}
50+
}
51+
52+
private fun TransportHttpBody.toEntity(): HttpEntity? = when (this) {
53+
is TransportHttpBody.EmptyBody ->
54+
null
55+
is TransportHttpBody.InputStreamBody ->
56+
BufferedHttpEntity(InputStreamEntity(stream, mimeType.toContentType()))
57+
is TransportHttpBody.ByteArrayBody ->
58+
ByteArrayEntity(byteArray, mimeType.toContentType())
59+
is TransportHttpBody.LocalFileBody ->
60+
FileEntity(file, mimeType.toContentType())
61+
}
62+
63+
private fun String.toContentType(): ContentType = ContentType.create(this, Charsets.UTF_8)
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.ecwid.apiclient.v3.httptransport.impl.client5
2+
3+
import com.ecwid.apiclient.v3.httptransport.HttpRequest
4+
import com.ecwid.apiclient.v3.httptransport.HttpResponse
5+
import org.apache.hc.client5.http.classic.HttpClient
6+
7+
class NoRetryRateLimitRetryStrategy : RateLimitRetryStrategy {
8+
override fun makeHttpRequest(httpClient: HttpClient, httpRequest: HttpRequest): HttpResponse {
9+
return httpClient.execute(httpRequest.toHttpUriRequest()) { response ->
10+
response.toApiResponse()
11+
}
12+
}
13+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package com.ecwid.apiclient.v3.httptransport.impl.client5
2+
3+
import com.ecwid.apiclient.v3.httptransport.HttpRequest
4+
import com.ecwid.apiclient.v3.httptransport.HttpResponse
5+
import org.apache.hc.client5.http.classic.HttpClient
6+
7+
interface RateLimitRetryStrategy {
8+
fun makeHttpRequest(httpClient: HttpClient, httpRequest: HttpRequest): HttpResponse
9+
}
10+

0 commit comments

Comments
 (0)