From e4f0e116f44b1fe14ba462602db621f56c26e853 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 4 May 2026 23:28:05 +0000 Subject: [PATCH 1/3] perf(client): create one json mapper --- .../src/main/kotlin/com/increase/api/core/ObjectMappers.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/increase-java-core/src/main/kotlin/com/increase/api/core/ObjectMappers.kt b/increase-java-core/src/main/kotlin/com/increase/api/core/ObjectMappers.kt index 87ef16436..78ed15c6f 100644 --- a/increase-java-core/src/main/kotlin/com/increase/api/core/ObjectMappers.kt +++ b/increase-java-core/src/main/kotlin/com/increase/api/core/ObjectMappers.kt @@ -29,7 +29,9 @@ import java.time.ZoneId import java.time.format.DateTimeFormatter import java.time.temporal.ChronoField -fun jsonMapper(): JsonMapper = +fun jsonMapper(): JsonMapper = JSON_MAPPER + +private val JSON_MAPPER: JsonMapper = JsonMapper.builder() .addModule(kotlinModule()) .addModule(Jdk8Module()) From 3026992f800d7dfdfb15271e9e2b62fa120400f2 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 5 May 2026 18:52:35 +0000 Subject: [PATCH 2/3] feat(client): support proxy authentication --- README.md | 15 ++ .../api/client/okhttp/IncreaseOkHttpClient.kt | 17 ++ .../okhttp/IncreaseOkHttpClientAsync.kt | 17 ++ .../api/client/okhttp/OkHttpClient.kt | 234 +++++++++++------- .../api/core/http/ProxyAuthenticator.kt | 59 +++++ 5 files changed, 257 insertions(+), 85 deletions(-) create mode 100644 increase-java-core/src/main/kotlin/com/increase/api/core/http/ProxyAuthenticator.kt diff --git a/README.md b/README.md index 7debd43e8..8bd8db3f9 100644 --- a/README.md +++ b/README.md @@ -528,6 +528,21 @@ IncreaseClient client = IncreaseOkHttpClient.builder() .build(); ``` +If the proxy responds with `407 Proxy Authentication Required`, supply credentials by also configuring `proxyAuthenticator`: + +```java +import com.increase.api.client.IncreaseClient; +import com.increase.api.client.okhttp.IncreaseOkHttpClient; +import com.increase.api.core.http.ProxyAuthenticator; + +IncreaseClient client = IncreaseOkHttpClient.builder() + .fromEnv() + .proxy(...) + // Or a custom implementation of `ProxyAuthenticator`. + .proxyAuthenticator(ProxyAuthenticator.basic("username", "password")) + .build(); +``` + ### Connection pooling To customize the underlying OkHttp connection pool, configure the client using the `maxIdleConnections` and `keepAliveDuration` methods: diff --git a/increase-java-client-okhttp/src/main/kotlin/com/increase/api/client/okhttp/IncreaseOkHttpClient.kt b/increase-java-client-okhttp/src/main/kotlin/com/increase/api/client/okhttp/IncreaseOkHttpClient.kt index 085bd79a4..7e6a40083 100644 --- a/increase-java-client-okhttp/src/main/kotlin/com/increase/api/client/okhttp/IncreaseOkHttpClient.kt +++ b/increase-java-client-okhttp/src/main/kotlin/com/increase/api/client/okhttp/IncreaseOkHttpClient.kt @@ -11,6 +11,7 @@ import com.increase.api.core.Timeout import com.increase.api.core.http.AsyncStreamResponse import com.increase.api.core.http.Headers import com.increase.api.core.http.HttpClient +import com.increase.api.core.http.ProxyAuthenticator import com.increase.api.core.http.QueryParams import com.increase.api.core.jsonMapper import java.net.Proxy @@ -49,6 +50,7 @@ class IncreaseOkHttpClient private constructor() { private var clientOptions: ClientOptions.Builder = ClientOptions.builder() private var dispatcherExecutorService: ExecutorService? = null private var proxy: Proxy? = null + private var proxyAuthenticator: ProxyAuthenticator? = null private var maxIdleConnections: Int? = null private var keepAliveDuration: Duration? = null private var sslSocketFactory: SSLSocketFactory? = null @@ -79,6 +81,20 @@ class IncreaseOkHttpClient private constructor() { /** Alias for calling [Builder.proxy] with `proxy.orElse(null)`. */ fun proxy(proxy: Optional) = proxy(proxy.getOrNull()) + /** + * Provides credentials when an HTTP proxy responds with `407 Proxy Authentication + * Required`. + */ + fun proxyAuthenticator(proxyAuthenticator: ProxyAuthenticator?) = apply { + this.proxyAuthenticator = proxyAuthenticator + } + + /** + * Alias for calling [Builder.proxyAuthenticator] with `proxyAuthenticator.orElse(null)`. + */ + fun proxyAuthenticator(proxyAuthenticator: Optional) = + proxyAuthenticator(proxyAuthenticator.getOrNull()) + /** * The maximum number of idle connections kept by the underlying OkHttp connection pool. * @@ -389,6 +405,7 @@ class IncreaseOkHttpClient private constructor() { OkHttpClient.builder() .timeout(clientOptions.timeout()) .proxy(proxy) + .proxyAuthenticator(proxyAuthenticator) .maxIdleConnections(maxIdleConnections) .keepAliveDuration(keepAliveDuration) .dispatcherExecutorService(dispatcherExecutorService) diff --git a/increase-java-client-okhttp/src/main/kotlin/com/increase/api/client/okhttp/IncreaseOkHttpClientAsync.kt b/increase-java-client-okhttp/src/main/kotlin/com/increase/api/client/okhttp/IncreaseOkHttpClientAsync.kt index 2c5d6c476..64ff30642 100644 --- a/increase-java-client-okhttp/src/main/kotlin/com/increase/api/client/okhttp/IncreaseOkHttpClientAsync.kt +++ b/increase-java-client-okhttp/src/main/kotlin/com/increase/api/client/okhttp/IncreaseOkHttpClientAsync.kt @@ -11,6 +11,7 @@ import com.increase.api.core.Timeout import com.increase.api.core.http.AsyncStreamResponse import com.increase.api.core.http.Headers import com.increase.api.core.http.HttpClient +import com.increase.api.core.http.ProxyAuthenticator import com.increase.api.core.http.QueryParams import com.increase.api.core.jsonMapper import java.net.Proxy @@ -49,6 +50,7 @@ class IncreaseOkHttpClientAsync private constructor() { private var clientOptions: ClientOptions.Builder = ClientOptions.builder() private var dispatcherExecutorService: ExecutorService? = null private var proxy: Proxy? = null + private var proxyAuthenticator: ProxyAuthenticator? = null private var maxIdleConnections: Int? = null private var keepAliveDuration: Duration? = null private var sslSocketFactory: SSLSocketFactory? = null @@ -79,6 +81,20 @@ class IncreaseOkHttpClientAsync private constructor() { /** Alias for calling [Builder.proxy] with `proxy.orElse(null)`. */ fun proxy(proxy: Optional) = proxy(proxy.getOrNull()) + /** + * Provides credentials when an HTTP proxy responds with `407 Proxy Authentication + * Required`. + */ + fun proxyAuthenticator(proxyAuthenticator: ProxyAuthenticator?) = apply { + this.proxyAuthenticator = proxyAuthenticator + } + + /** + * Alias for calling [Builder.proxyAuthenticator] with `proxyAuthenticator.orElse(null)`. + */ + fun proxyAuthenticator(proxyAuthenticator: Optional) = + proxyAuthenticator(proxyAuthenticator.getOrNull()) + /** * The maximum number of idle connections kept by the underlying OkHttp connection pool. * @@ -389,6 +405,7 @@ class IncreaseOkHttpClientAsync private constructor() { OkHttpClient.builder() .timeout(clientOptions.timeout()) .proxy(proxy) + .proxyAuthenticator(proxyAuthenticator) .maxIdleConnections(maxIdleConnections) .keepAliveDuration(keepAliveDuration) .dispatcherExecutorService(dispatcherExecutorService) diff --git a/increase-java-client-okhttp/src/main/kotlin/com/increase/api/client/okhttp/OkHttpClient.kt b/increase-java-client-okhttp/src/main/kotlin/com/increase/api/client/okhttp/OkHttpClient.kt index 3f74e40e4..8343acf51 100644 --- a/increase-java-client-okhttp/src/main/kotlin/com/increase/api/client/okhttp/OkHttpClient.kt +++ b/increase-java-client-okhttp/src/main/kotlin/com/increase/api/client/okhttp/OkHttpClient.kt @@ -8,9 +8,11 @@ import com.increase.api.core.http.HttpMethod import com.increase.api.core.http.HttpRequest import com.increase.api.core.http.HttpRequestBody import com.increase.api.core.http.HttpResponse +import com.increase.api.core.http.ProxyAuthenticator import com.increase.api.errors.IncreaseIoException import java.io.IOException import java.io.InputStream +import java.io.OutputStream import java.net.Proxy import java.time.Duration import java.util.concurrent.CancellationException @@ -20,10 +22,12 @@ import java.util.concurrent.TimeUnit import javax.net.ssl.HostnameVerifier import javax.net.ssl.SSLSocketFactory import javax.net.ssl.X509TrustManager +import kotlin.jvm.optionals.getOrNull import okhttp3.Call import okhttp3.Callback import okhttp3.ConnectionPool import okhttp3.Dispatcher +import okhttp3.HttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.MediaType import okhttp3.MediaType.Companion.toMediaType @@ -33,6 +37,8 @@ import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.Response import okhttp3.logging.HttpLoggingInterceptor import okio.BufferedSink +import okio.buffer +import okio.sink class OkHttpClient internal constructor(@JvmSynthetic internal val okHttpClient: okhttp3.OkHttpClient) : HttpClient { @@ -41,7 +47,7 @@ internal constructor(@JvmSynthetic internal val okHttpClient: okhttp3.OkHttpClie val call = newCall(request, requestOptions) return try { - call.execute().toResponse() + call.execute().toHttpResponse() } catch (e: IOException) { throw IncreaseIoException("Request failed", e) } finally { @@ -59,7 +65,7 @@ internal constructor(@JvmSynthetic internal val okHttpClient: okhttp3.OkHttpClie call.enqueue( object : Callback { override fun onResponse(call: Call, response: Response) { - future.complete(response.toResponse()) + future.complete(response.toHttpResponse()) } override fun onFailure(call: Call, e: IOException) { @@ -111,89 +117,6 @@ internal constructor(@JvmSynthetic internal val okHttpClient: okhttp3.OkHttpClie return client.newCall(request.toRequest(client)) } - private fun HttpRequest.toRequest(client: okhttp3.OkHttpClient): Request { - var body: RequestBody? = body?.toRequestBody() - if (body == null && requiresBody(method)) { - body = "".toRequestBody() - } - - val builder = Request.Builder().url(toUrl()).method(method.name, body) - headers.names().forEach { name -> - headers.values(name).forEach { builder.addHeader(name, it) } - } - - if ( - !headers.names().contains("X-Stainless-Read-Timeout") && client.readTimeoutMillis != 0 - ) { - builder.addHeader( - "X-Stainless-Read-Timeout", - Duration.ofMillis(client.readTimeoutMillis.toLong()).seconds.toString(), - ) - } - if (!headers.names().contains("X-Stainless-Timeout") && client.callTimeoutMillis != 0) { - builder.addHeader( - "X-Stainless-Timeout", - Duration.ofMillis(client.callTimeoutMillis.toLong()).seconds.toString(), - ) - } - - return builder.build() - } - - /** `OkHttpClient` always requires a request body for some methods. */ - private fun requiresBody(method: HttpMethod): Boolean = - when (method) { - HttpMethod.POST, - HttpMethod.PUT, - HttpMethod.PATCH -> true - else -> false - } - - private fun HttpRequest.toUrl(): String { - val builder = baseUrl.toHttpUrl().newBuilder() - pathSegments.forEach(builder::addPathSegment) - queryParams.keys().forEach { key -> - queryParams.values(key).forEach { builder.addQueryParameter(key, it) } - } - - return builder.toString() - } - - private fun HttpRequestBody.toRequestBody(): RequestBody { - val mediaType = contentType()?.toMediaType() - val length = contentLength() - - return object : RequestBody() { - override fun contentType(): MediaType? = mediaType - - override fun contentLength(): Long = length - - override fun isOneShot(): Boolean = !repeatable() - - override fun writeTo(sink: BufferedSink) = writeTo(sink.outputStream()) - } - } - - private fun Response.toResponse(): HttpResponse { - val headers = headers.toHeaders() - - return object : HttpResponse { - override fun statusCode(): Int = code - - override fun headers(): Headers = headers - - override fun body(): InputStream = body!!.byteStream() - - override fun close() = body!!.close() - } - } - - private fun okhttp3.Headers.toHeaders(): Headers { - val headersBuilder = Headers.builder() - forEach { (name, value) -> headersBuilder.put(name, value) } - return headersBuilder.build() - } - companion object { @JvmStatic fun builder() = Builder() } @@ -202,6 +125,7 @@ internal constructor(@JvmSynthetic internal val okHttpClient: okhttp3.OkHttpClie private var timeout: Timeout = Timeout.default() private var proxy: Proxy? = null + private var proxyAuthenticator: ProxyAuthenticator? = null private var maxIdleConnections: Int? = null private var keepAliveDuration: Duration? = null private var dispatcherExecutorService: ExecutorService? = null @@ -215,6 +139,10 @@ internal constructor(@JvmSynthetic internal val okHttpClient: okhttp3.OkHttpClie fun proxy(proxy: Proxy?) = apply { this.proxy = proxy } + fun proxyAuthenticator(proxyAuthenticator: ProxyAuthenticator?) = apply { + this.proxyAuthenticator = proxyAuthenticator + } + /** * Sets the maximum number of idle connections kept by the underlying [ConnectionPool]. * @@ -264,6 +192,19 @@ internal constructor(@JvmSynthetic internal val okHttpClient: okhttp3.OkHttpClie .callTimeout(timeout.request()) .proxy(proxy) .apply { + proxyAuthenticator?.let { auth -> + proxyAuthenticator { route, response -> + auth + .authenticate( + route?.proxy ?: Proxy.NO_PROXY, + response.request.toHttpRequest(), + response.toHttpResponse(), + ) + .getOrNull() + ?.toRequest(client = null) + } + } + dispatcherExecutorService?.let { dispatcher(Dispatcher(it)) } val maxIdleConnections = maxIdleConnections @@ -303,3 +244,126 @@ internal constructor(@JvmSynthetic internal val okHttpClient: okhttp3.OkHttpClie ) } } + +private fun HttpRequest.toRequest(client: okhttp3.OkHttpClient?): Request { + var body: RequestBody? = body?.toRequestBody() + if (body == null && requiresBody(method)) { + body = "".toRequestBody() + } + + val builder = Request.Builder().url(toUrl()).method(method.name, body) + headers.names().forEach { name -> headers.values(name).forEach { builder.addHeader(name, it) } } + + if (client != null) { + if ( + !headers.names().contains("X-Stainless-Read-Timeout") && client.readTimeoutMillis != 0 + ) { + builder.addHeader( + "X-Stainless-Read-Timeout", + Duration.ofMillis(client.readTimeoutMillis.toLong()).seconds.toString(), + ) + } + if (!headers.names().contains("X-Stainless-Timeout") && client.callTimeoutMillis != 0) { + builder.addHeader( + "X-Stainless-Timeout", + Duration.ofMillis(client.callTimeoutMillis.toLong()).seconds.toString(), + ) + } + } + + return builder.build() +} + +/** `OkHttpClient` always requires a request body for some methods. */ +private fun requiresBody(method: HttpMethod): Boolean = + when (method) { + HttpMethod.POST, + HttpMethod.PUT, + HttpMethod.PATCH -> true + else -> false + } + +private fun HttpRequest.toUrl(): String { + val builder = baseUrl.toHttpUrl().newBuilder() + pathSegments.forEach(builder::addPathSegment) + queryParams.keys().forEach { key -> + queryParams.values(key).forEach { builder.addQueryParameter(key, it) } + } + + return builder.toString() +} + +private fun HttpRequestBody.toRequestBody(): RequestBody { + val mediaType = contentType()?.toMediaType() + val length = contentLength() + + return object : RequestBody() { + override fun contentType(): MediaType? = mediaType + + override fun contentLength(): Long = length + + override fun isOneShot(): Boolean = !repeatable() + + override fun writeTo(sink: BufferedSink) = writeTo(sink.outputStream()) + } +} + +private fun Request.toHttpRequest(): HttpRequest { + val builder = HttpRequest.builder().method(HttpMethod.valueOf(method)).baseUrl(url.toBaseUrl()) + url.pathSegments.forEach(builder::addPathSegment) + url.queryParameterNames.forEach { name -> + url.queryParameterValues(name).filterNotNull().forEach { builder.putQueryParam(name, it) } + } + headers.forEach { (name, value) -> builder.putHeader(name, value) } + body?.let { builder.body(it.toHttpRequestBody()) } + return builder.build() +} + +private fun HttpUrl.toBaseUrl(): String = buildString { + append(scheme).append("://").append(host) + if (port != HttpUrl.defaultPort(scheme)) { + append(":").append(port) + } +} + +private fun RequestBody.toHttpRequestBody(): HttpRequestBody { + val mediaType = contentType()?.toString() + val length = contentLength() + val isOneShot = isOneShot() + val source = this + return object : HttpRequestBody { + override fun contentType(): String? = mediaType + + override fun contentLength(): Long = length + + override fun repeatable(): Boolean = !isOneShot + + override fun writeTo(outputStream: OutputStream) { + val sink = outputStream.sink().buffer() + source.writeTo(sink) + sink.flush() + } + + override fun close() {} + } +} + +private fun Response.toHttpResponse(): HttpResponse { + val headers = headers.toHeaders() + + return object : HttpResponse { + override fun statusCode(): Int = code + + override fun headers(): Headers = headers + + override fun body(): InputStream = body!!.byteStream() + + override fun close() = body!!.close() + } +} + +private fun okhttp3.Headers.toHeaders(): Headers { + val headersBuilder = Headers.builder() + forEach { (name, value) -> headersBuilder.put(name, value) } + return headersBuilder.build() +} diff --git a/increase-java-core/src/main/kotlin/com/increase/api/core/http/ProxyAuthenticator.kt b/increase-java-core/src/main/kotlin/com/increase/api/core/http/ProxyAuthenticator.kt new file mode 100644 index 000000000..fcb93515e --- /dev/null +++ b/increase-java-core/src/main/kotlin/com/increase/api/core/http/ProxyAuthenticator.kt @@ -0,0 +1,59 @@ +package com.increase.api.core.http + +import java.net.Proxy +import java.nio.charset.Charset +import java.nio.charset.StandardCharsets +import java.util.Base64 +import java.util.Optional + +/** + * Provides credentials when an HTTP proxy responds with `407 Proxy Authentication Required`. + * + * Implementations inspect the 407 [response] (typically its `Proxy-Authenticate` header) and return + * the request to retry with a `Proxy-Authorization` header set, or [Optional.empty] to abandon + * authentication and surface the 407 to the caller. + * + * Implementations must be thread-safe; they may be invoked concurrently from multiple HTTP calls. + */ +fun interface ProxyAuthenticator { + + /** + * @param proxy the proxy that produced the challenge, or [Proxy.NO_PROXY] if the route is not + * yet established + * @param request the request that produced [response] + * @param response the 407 challenge response + * @return the retry request to send (typically [request] with a `Proxy-Authorization` header + * added), or [Optional.empty] to abandon authentication + */ + fun authenticate( + proxy: Proxy, + request: HttpRequest, + response: HttpResponse, + ): Optional + + companion object { + + /** + * A [ProxyAuthenticator] that uses RFC 7617 Basic authentication with the ISO-8859-1 + * charset. + */ + @JvmStatic + fun basic(username: String, password: String): ProxyAuthenticator = + basic(username, password, StandardCharsets.ISO_8859_1) + + /** + * A [ProxyAuthenticator] that uses RFC 7617 Basic authentication with the given [charset]. + */ + @JvmStatic + fun basic(username: String, password: String, charset: Charset): ProxyAuthenticator { + val token = + Base64.getEncoder().encodeToString("$username:$password".toByteArray(charset)) + val headerValue = "Basic $token" + return ProxyAuthenticator { _, request, _ -> + Optional.of( + request.toBuilder().putHeader("Proxy-Authorization", headerValue).build() + ) + } + } + } +} From a166725b28ccfddc4d93e309217ef1385cc221f9 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 5 May 2026 18:53:28 +0000 Subject: [PATCH 3/3] release: 0.543.0 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 13 +++++++++++++ README.md | 10 +++++----- build.gradle.kts | 2 +- 4 files changed, 20 insertions(+), 7 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index b42ccb36a..79a99582e 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.542.0" + ".": "0.543.0" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index cee27c3f6..b47d4a4f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## 0.543.0 (2026-05-05) + +Full Changelog: [v0.542.0...v0.543.0](https://github.com/Increase/increase-java/compare/v0.542.0...v0.543.0) + +### Features + +* **client:** support proxy authentication ([3026992](https://github.com/Increase/increase-java/commit/3026992f800d7dfdfb15271e9e2b62fa120400f2)) + + +### Performance Improvements + +* **client:** create one json mapper ([e4f0e11](https://github.com/Increase/increase-java/commit/e4f0e116f44b1fe14ba462602db621f56c26e853)) + ## 0.542.0 (2026-05-04) Full Changelog: [v0.541.0...v0.542.0](https://github.com/Increase/increase-java/compare/v0.541.0...v0.542.0) diff --git a/README.md b/README.md index 8bd8db3f9..0d154e0c3 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ -[![Maven Central](https://img.shields.io/maven-central/v/com.increase.api/increase-java)](https://central.sonatype.com/artifact/com.increase.api/increase-java/0.542.0) -[![javadoc](https://javadoc.io/badge2/com.increase.api/increase-java/0.542.0/javadoc.svg)](https://javadoc.io/doc/com.increase.api/increase-java/0.542.0) +[![Maven Central](https://img.shields.io/maven-central/v/com.increase.api/increase-java)](https://central.sonatype.com/artifact/com.increase.api/increase-java/0.543.0) +[![javadoc](https://javadoc.io/badge2/com.increase.api/increase-java/0.543.0/javadoc.svg)](https://javadoc.io/doc/com.increase.api/increase-java/0.543.0) @@ -13,7 +13,7 @@ The Increase Java SDK is similar to the Increase Kotlin SDK but with minor diffe -The REST API documentation can be found on [increase.com](https://increase.com/documentation). Javadocs are available on [javadoc.io](https://javadoc.io/doc/com.increase.api/increase-java/0.542.0). +The REST API documentation can be found on [increase.com](https://increase.com/documentation). Javadocs are available on [javadoc.io](https://javadoc.io/doc/com.increase.api/increase-java/0.543.0). @@ -24,7 +24,7 @@ The REST API documentation can be found on [increase.com](https://increase.com/d ### Gradle ```kotlin -implementation("com.increase.api:increase-java:0.542.0") +implementation("com.increase.api:increase-java:0.543.0") ``` ### Maven @@ -33,7 +33,7 @@ implementation("com.increase.api:increase-java:0.542.0") com.increase.api increase-java - 0.542.0 + 0.543.0 ``` diff --git a/build.gradle.kts b/build.gradle.kts index a94429eac..c826db72e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -8,7 +8,7 @@ repositories { allprojects { group = "com.increase.api" - version = "0.542.0" // x-release-please-version + version = "0.543.0" // x-release-please-version } subprojects {