Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 107 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -974,6 +974,113 @@ AnthropicClient client = AnthropicOkHttpClient.builder()
.build();
```

### Interceptors

To intercept all requests and responses _after_ [retries](#retries), configure the client using the `interceptor` method:

```java
import com.anthropic.client.AnthropicClient;
import com.anthropic.client.okhttp.AnthropicOkHttpClient;
import com.anthropic.core.RequestOptions;
import com.anthropic.core.http.HttpClient;
import com.anthropic.core.http.HttpRequest;
import com.anthropic.core.http.HttpResponse;
import java.util.concurrent.CompletableFuture;

class LoggingHttpClient implements HttpClient {

private final HttpClient delegate;

LoggingHttpClient(HttpClient delegate) {
this.delegate = delegate;
}

@Override
public HttpResponse execute(
HttpRequest request,
RequestOptions requestOptions
) {
System.out.println("Sending request...");
HttpResponse response = delegate.execute(
// Optionally modify the request
request.toBuilder().putHeader("X-Request-ID", "42").build(),
requestOptions
);
System.out.println("Received response!");
// Optionally modify the response by implementing `HttpResponse`
return response;
}

@Override
public CompletableFuture<HttpResponse> executeAsync(
HttpRequest request,
RequestOptions requestOptions
) {
System.out.println("Sending request...");
CompletableFuture<HttpResponse> responseFuture = delegate.executeAsync(
// Optionally modify the request
request.toBuilder().putHeader("X-Request-ID", "42").build(),
requestOptions
);
return responseFuture.thenApply(response -> {
System.out.println("Received response!");
// Optionally modify the response by implementing `HttpResponse`
return response;
});
}

@Override
public void close() {
delegate.close();
}
}

AnthropicClient client = AnthropicOkHttpClient.builder()
.fromEnv()
// Or pass a lambda
.interceptor(LoggingHttpClient::new)
.build();
```

To intercept _before_ retries, which is useful for logging and tracing, configure the client using the `networkInterceptor` method instead:

```java
import com.anthropic.client.AnthropicClient;
import com.anthropic.client.okhttp.AnthropicOkHttpClient;

AnthropicClient client = AnthropicOkHttpClient.builder()
.fromEnv()
// Or pass a lambda
.networkInterceptor(LoggingHttpClient::new)
.build();
```

Or configure the client to intercept synchronous calls only, if you don't use [asynchronous execution](#asynchronous-execution):

```java
import com.anthropic.client.AnthropicClient;
import com.anthropic.client.okhttp.AnthropicOkHttpClient;
import com.anthropic.core.http.HttpResponse;
import com.anthropic.core.http.Interceptor;

AnthropicClient client = AnthropicOkHttpClient.builder()
.fromEnv()
// Or `networkInterceptor`
.interceptor(Interceptor.syncOnly((httpClient, request, requestOptions) -> {
System.out.println("Sending request...");
HttpResponse response = httpClient.execute(request, response);
System.out.println("Received response!");
return response;
}))
.build();
```

Or configure the client to intercept asynchronous calls only, if you _only_ use asynchronous execution, using `Interceptor.asyncOnly`.

> [!NOTE]
> Only a single `interceptor` and a single `networkInterceptor` can be configured. To configure multiple

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Many users are used to chaining interceptors (or middleware) instead of one big call. Given this can also be done conditionally (for perf reasons for ex.), I think it makes for a better API than enforce all the wrapping in a single call.

Ex. https://square.github.io/okhttp/features/interceptors/

but you can also see this generally with python, js, ruby, java server library middlewares

Copy link
Collaborator Author

@TomerAberbach TomerAberbach Sep 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We actually looked at the OkHttp API and considered using the same chaining API, but we felt that the chaining API makes it more difficult to understand the order things run. i.e. does addInterceptor(A).addInterceptor(B) result in A(B(...)) or B(A(...)), while if you do the wrapping yourself the order is crystal clear.

It's possible we're overthinking this though; in your experience have folks been confused about ordering of these kinds of things?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in your experience have folks been confused about ordering of these kinds of things

There's a general understanding about top-to-bottom/left-to-right ordering, like with GRPC or middleware chains for web server frameworks (think Express for JavaScript or Gin for Go) in most programming language ecosystems. I wouldn't worry about this as an issue from my experience working on the Sentry SDKs.

> layers of wrapping, perform all the wrapping in a single call.

### Custom HTTP client

The SDK consists of three artifacts:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import com.anthropic.client.AnthropicClientImpl
import com.anthropic.core.ClientOptions
import com.anthropic.core.Timeout
import com.anthropic.core.http.Headers
import com.anthropic.core.http.Interceptor
import com.anthropic.core.http.QueryParams
import com.anthropic.core.jsonMapper
import com.fasterxml.jackson.databind.json.JsonMapper
Expand Down Expand Up @@ -96,6 +97,41 @@ class AnthropicOkHttpClient private constructor() {
fun hostnameVerifier(hostnameVerifier: Optional<HostnameVerifier>) =
hostnameVerifier(hostnameVerifier.getOrNull())

/**
* Wraps the HTTP client using the given [interceptor].
*
* The HTTP client may perform retries. Use [networkInterceptor] to wrap the raw HTTP client
* before retry logic.
*
* Also note that calling [interceptor] multiple times overwrites the previous call. To
* apply multiple layers of wrapping, perform all the wrapping in a single call.
*/
fun interceptor(interceptor: Interceptor?) = apply {
clientOptions.interceptor(interceptor)
}

/** Alias for calling [Builder.interceptor] with `interceptor.orElse(null)`. */
fun interceptor(interceptor: Optional<Interceptor>) = interceptor(interceptor.getOrNull())

/**
* Wraps the raw HTTP client using the given [interceptor].
*
* The raw HTTP client does _not_ perform retries. Use [interceptor] to wrap the HTTP client
* after retry logic.
*
* Also note that calling [networkInterceptor] multiple times overwrites the previous call.
* To apply multiple layers of wrapping, perform all the wrapping in a single call.
*/
fun networkInterceptor(networkInterceptor: Interceptor?) = apply {
clientOptions.networkInterceptor(networkInterceptor)
}

/**
* Alias for calling [Builder.networkInterceptor] with `networkInterceptor.orElse(null)`.
*/
fun networkInterceptor(networkInterceptor: Optional<Interceptor>) =
networkInterceptor(networkInterceptor.getOrNull())

/**
* Whether to throw an exception if any of the Jackson versions detected at runtime are
* incompatible with the SDK's minimum supported Jackson version (2.13.4).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import com.anthropic.client.AnthropicClientAsyncImpl
import com.anthropic.core.ClientOptions
import com.anthropic.core.Timeout
import com.anthropic.core.http.Headers
import com.anthropic.core.http.Interceptor
import com.anthropic.core.http.QueryParams
import com.anthropic.core.jsonMapper
import com.fasterxml.jackson.databind.json.JsonMapper
Expand Down Expand Up @@ -98,6 +99,41 @@ class AnthropicOkHttpClientAsync private constructor() {
fun hostnameVerifier(hostnameVerifier: Optional<HostnameVerifier>) =
hostnameVerifier(hostnameVerifier.getOrNull())

/**
* Wraps the HTTP client using the given [interceptor].
*
* The HTTP client may perform retries. Use [networkInterceptor] to wrap the raw HTTP client
* before retry logic.
*
* Also note that calling [interceptor] multiple times overwrites the previous call. To
* apply multiple layers of wrapping, perform all the wrapping in a single call.
*/
fun interceptor(interceptor: Interceptor?) = apply {
clientOptions.interceptor(interceptor)
}

/** Alias for calling [Builder.interceptor] with `interceptor.orElse(null)`. */
fun interceptor(interceptor: Optional<Interceptor>) = interceptor(interceptor.getOrNull())

/**
* Wraps the raw HTTP client using the given [interceptor].
*
* The raw HTTP client does _not_ perform retries. Use [interceptor] to wrap the HTTP client
* after retry logic.
*
* Also note that calling [networkInterceptor] multiple times overwrites the previous call.
* To apply multiple layers of wrapping, perform all the wrapping in a single call.
*/
fun networkInterceptor(networkInterceptor: Interceptor?) = apply {
clientOptions.networkInterceptor(networkInterceptor)
}

/**
* Alias for calling [Builder.networkInterceptor] with `networkInterceptor.orElse(null)`.
*/
fun networkInterceptor(networkInterceptor: Optional<Interceptor>) =
networkInterceptor(networkInterceptor.getOrNull())

/**
* Whether to throw an exception if any of the Jackson versions detected at runtime are
* incompatible with the SDK's minimum supported Jackson version (2.13.4).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ package com.anthropic.core

import com.anthropic.core.http.Headers
import com.anthropic.core.http.HttpClient
import com.anthropic.core.http.Interceptor
import com.anthropic.core.http.PhantomReachableClosingHttpClient
import com.anthropic.core.http.QueryParams
import com.anthropic.core.http.RetryingHttpClient
import com.anthropic.core.http.intercept
import com.fasterxml.jackson.databind.json.JsonMapper
import java.time.Clock
import java.time.Duration
Expand All @@ -21,6 +23,8 @@ class ClientOptions
private constructor(
private val originalHttpClient: HttpClient,
@get:JvmName("httpClient") val httpClient: HttpClient,
private val interceptor: Interceptor?,
private val networkInterceptor: Interceptor?,
/**
* Whether to throw an exception if any of the Jackson versions detected at runtime are
* incompatible with the SDK's minimum supported Jackson version (2.13.4).
Expand All @@ -46,6 +50,28 @@ private constructor(
}
}

/**
* Wraps the HTTP client using the given [interceptor].
*
* The HTTP client may perform retries. Use [networkInterceptor] to wrap the raw HTTP client
* before retry logic.
*
* Also note that calling [interceptor] multiple times overwrites the previous call. To apply
* multiple layers of wrapping, perform all the wrapping in a single call.
*/
fun interceptor(): Optional<Interceptor> = Optional.ofNullable(interceptor)

/**
* Wraps the raw HTTP client using the given [interceptor].
*
* The raw HTTP client does _not_ perform retries. Use [interceptor] to wrap the HTTP client
* after retry logic.
*
* Also note that calling [networkInterceptor] multiple times overwrites the previous call. To
* apply multiple layers of wrapping, perform all the wrapping in a single call.
*/
fun networkInterceptor(): Optional<Interceptor> = Optional.ofNullable(networkInterceptor)

fun baseUrl(): String? = baseUrl

fun toBuilder() = Builder().from(this)
Expand All @@ -67,6 +93,8 @@ private constructor(
class Builder internal constructor() {

private var httpClient: HttpClient? = null
private var interceptor: Interceptor? = null
private var networkInterceptor: Interceptor? = null
private var checkJacksonVersionCompatibility: Boolean = true
private var jsonMapper: JsonMapper = jsonMapper()
private var streamHandlerExecutor: Executor? = null
Expand All @@ -81,6 +109,8 @@ private constructor(
@JvmSynthetic
internal fun from(clientOptions: ClientOptions) = apply {
httpClient = clientOptions.originalHttpClient
interceptor = clientOptions.interceptor
networkInterceptor = clientOptions.networkInterceptor
checkJacksonVersionCompatibility = clientOptions.checkJacksonVersionCompatibility
jsonMapper = clientOptions.jsonMapper
streamHandlerExecutor = clientOptions.streamHandlerExecutor
Expand All @@ -97,6 +127,39 @@ private constructor(
this.httpClient = PhantomReachableClosingHttpClient(httpClient)
}

/**
* Wraps the HTTP client using the given [interceptor].
*
* The HTTP client may perform retries. Use [networkInterceptor] to wrap the raw HTTP client
* before retry logic.
*
* Also note that calling [interceptor] multiple times overwrites the previous call. To
* apply multiple layers of wrapping, perform all the wrapping in a single call.
*/
fun interceptor(interceptor: Interceptor?) = apply { this.interceptor = interceptor }

/** Alias for calling [Builder.interceptor] with `interceptor.orElse(null)`. */
fun interceptor(interceptor: Optional<Interceptor>) = interceptor(interceptor.getOrNull())

/**
* Wraps the raw HTTP client using the given [interceptor].
*
* The raw HTTP client does _not_ perform retries. Use [interceptor] to wrap the HTTP client
* after retry logic.
*
* Also note that calling [networkInterceptor] multiple times overwrites the previous call.
* To apply multiple layers of wrapping, perform all the wrapping in a single call.
*/
fun networkInterceptor(networkInterceptor: Interceptor?) = apply {
this.networkInterceptor = networkInterceptor
}

/**
* Alias for calling [Builder.networkInterceptor] with `networkInterceptor.orElse(null)`.
*/
fun networkInterceptor(networkInterceptor: Optional<Interceptor>) =
networkInterceptor(networkInterceptor.getOrNull())

/**
* Whether to throw an exception if any of the Jackson versions detected at runtime are
* incompatible with the SDK's minimum supported Jackson version (2.13.4).
Expand Down Expand Up @@ -249,11 +312,21 @@ private constructor(

return ClientOptions(
httpClient,
RetryingHttpClient.builder()
.httpClient(httpClient)
.clock(clock)
.maxRetries(maxRetries)
.build(),
interceptor.intercept(
// Add default post-retries interceptors around this client.
RetryingHttpClient.builder()
.httpClient(
networkInterceptor.intercept(
// Add default pre-retries interceptors around this client.
httpClient
)
)
.clock(clock)
.maxRetries(maxRetries)
.build()
),
interceptor,
networkInterceptor,
checkJacksonVersionCompatibility,
jsonMapper,
streamHandlerExecutor
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// File generated from our OpenAPI spec by Stainless.

package com.anthropic.core.http

import com.anthropic.core.RequestOptions

fun interface HttpClientExecute {

fun execute(
httpClient: HttpClient,
request: HttpRequest,
requestOptions: RequestOptions,
): HttpResponse
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// File generated from our OpenAPI spec by Stainless.

package com.anthropic.core.http

import com.anthropic.core.RequestOptions
import java.util.concurrent.CompletableFuture

fun interface HttpClientExecuteAsync {

fun executeAsync(
httpClient: HttpClient,
request: HttpRequest,
requestOptions: RequestOptions,
): CompletableFuture<HttpResponse>
}
Loading