diff --git a/anthropic-java-client-okhttp/src/main/kotlin/com/anthropic/client/okhttp/OkHttpClient.kt b/anthropic-java-client-okhttp/src/main/kotlin/com/anthropic/client/okhttp/OkHttpClient.kt index 9716b2933..aa0e9bb72 100644 --- a/anthropic-java-client-okhttp/src/main/kotlin/com/anthropic/client/okhttp/OkHttpClient.kt +++ b/anthropic-java-client-okhttp/src/main/kotlin/com/anthropic/client/okhttp/OkHttpClient.kt @@ -232,6 +232,7 @@ internal constructor( private var sslSocketFactory: SSLSocketFactory? = null private var trustManager: X509TrustManager? = null private var hostnameVerifier: HostnameVerifier? = null + private val interceptors: MutableList = mutableListOf() fun timeout(timeout: Timeout) = apply { this.timeout = timeout } @@ -279,6 +280,33 @@ internal constructor( this.hostnameVerifier = hostnameVerifier } + /** + * Adds a custom OkHttp [okhttp3.Interceptor] that will be applied to every request. + * + * This is useful for cross-cutting concerns such as trace context propagation. For example, + * to forward an `X-Trace-Id` header from an incoming request using a [ThreadLocal]: + * + * ```kotlin + * val traceIdHolder = ThreadLocal() + * + * val httpClient = OkHttpClient.builder() + * .backend(backend) + * .addInterceptor { chain -> + * val traceId = traceIdHolder.get() + * val request = if (traceId != null) { + * chain.request().newBuilder().addHeader("X-Trace-Id", traceId).build() + * } else { + * chain.request() + * } + * chain.proceed(request) + * } + * .build() + * ``` + */ + fun addInterceptor(interceptor: okhttp3.Interceptor) = apply { + interceptors.add(interceptor) + } + fun build(): OkHttpClient = OkHttpClient( okhttp3.OkHttpClient.Builder() @@ -320,6 +348,8 @@ internal constructor( } hostnameVerifier?.let(::hostnameVerifier) + + interceptors.forEach { addInterceptor(it) } } .build() .apply { diff --git a/anthropic-java-client-okhttp/src/test/kotlin/com/anthropic/client/okhttp/OkHttpClientTest.kt b/anthropic-java-client-okhttp/src/test/kotlin/com/anthropic/client/okhttp/OkHttpClientTest.kt index 9ec156c2c..16a7a54e7 100644 --- a/anthropic-java-client-okhttp/src/test/kotlin/com/anthropic/client/okhttp/OkHttpClientTest.kt +++ b/anthropic-java-client-okhttp/src/test/kotlin/com/anthropic/client/okhttp/OkHttpClientTest.kt @@ -30,6 +30,43 @@ internal class OkHttpClientTest { httpClient = OkHttpClient.builder().backend(TestBackend(baseUrl)).build() } + @Test + fun addInterceptor_interceptorIsCalledAndCanAddHeaders() { + stubFor(post(urlPathEqualTo("/something")).willReturn(ok())) + val traceIdHolder = ThreadLocal() + val clientWithInterceptor = + OkHttpClient.builder() + .backend(TestBackend(baseUrl)) + .addInterceptor { chain -> + val traceId = traceIdHolder.get() + val request = + if (traceId != null) { + chain.request().newBuilder().addHeader("X-Trace-Id", traceId).build() + } else { + chain.request() + } + chain.proceed(request) + } + .build() + + traceIdHolder.set("test-trace-123") + try { + clientWithInterceptor + .execute( + HttpRequest.builder() + .method(HttpMethod.POST) + .baseUrl(baseUrl) + .addPathSegment("something") + .build() + ) + .close() + } finally { + traceIdHolder.remove() + } + + verify(postRequestedFor(urlPathEqualTo("/something")).withHeader("X-Trace-Id", equalTo("test-trace-123"))) + } + @Test fun executeAsync_whenFutureCancelled_cancelsUnderlyingCall() { stubFor(post(urlPathEqualTo("/something")).willReturn(ok()))