Skip to content
Merged
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
40 changes: 40 additions & 0 deletions src/main/java/com/stripe/net/HttpClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import java.util.Optional;
import java.util.Properties;
import java.util.concurrent.ThreadLocalRandom;
import java.util.function.Function;

/** Base abstract class for HTTP clients used to send requests to Stripe's API. */
public abstract class HttpClient {
Expand Down Expand Up @@ -137,12 +138,39 @@ public StripeResponseStream requestStreamWithRetries(StripeRequest request)
return sendWithRetries(request, (r) -> this.requestStream(r));
}

static String detectAIAgent() {
return detectAIAgent(System::getenv);
}

static String detectAIAgent(Function<String, String> getEnv) {
String[][] agents = {
{"ANTIGRAVITY_CLI_ALIAS", "antigravity"},
{"CLAUDECODE", "claude_code"},
{"CLINE_ACTIVE", "cline"},
{"CODEX_SANDBOX", "codex_cli"},
{"CURSOR_AGENT", "cursor"},
{"GEMINI_CLI", "gemini_cli"},
{"OPENCODE", "open_code"},
};
for (String[] agent : agents) {
String val = getEnv.apply(agent[0]);
if (val != null && !val.isEmpty()) {
return agent[1];
}
}
return "";
}

/**
* Builds the value of the {@code User-Agent} header.
*
* @return a string containing the value of the {@code User-Agent} header
*/
protected static String buildUserAgentString(StripeRequest request) {
return buildUserAgentString(request, detectAIAgent());
}

static String buildUserAgentString(StripeRequest request, String aiAgent) {
String apiMode = request.apiMode() == ApiMode.V2 ? "v2" : "v1";

String userAgent = String.format("Stripe/%s JavaBindings/%s", apiMode, Stripe.VERSION);
Expand All @@ -151,6 +179,10 @@ protected static String buildUserAgentString(StripeRequest request) {
userAgent += " " + formatAppInfo(Stripe.getAppInfo());
}

if (!aiAgent.isEmpty()) {
userAgent += " AIAgent/" + aiAgent;
}

return userAgent;
}

Expand All @@ -160,6 +192,10 @@ protected static String buildUserAgentString(StripeRequest request) {
* @return a string containing the value of the {@code X-Stripe-Client-User-Agent} header
*/
protected static String buildXStripeClientUserAgentString() {
return buildXStripeClientUserAgentString(detectAIAgent());
}

static String buildXStripeClientUserAgentString(String aiAgent) {
String[] propertyNames = {
"os.name",
"os.version",
Expand All @@ -182,6 +218,10 @@ protected static String buildXStripeClientUserAgentString() {
}
getGsonVersion().ifPresent(ver -> propertyMap.put("gson.version", ver));

if (!aiAgent.isEmpty()) {
propertyMap.put("ai_agent", aiAgent);
}

return ApiResource.INTERNAL_GSON.toJson(propertyMap);
}

Expand Down
55 changes: 49 additions & 6 deletions src/test/java/com/stripe/net/HttpClientTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -192,9 +192,8 @@ public void testV1RequestSetsCorrectUserAgent() throws StripeException {
RequestOptions.builder().setApiKey("sk_test_123").setMaxNetworkRetries(2).build(),
ApiMode.V1);

assertEquals(
HttpClient.buildUserAgentString(request),
String.format("Stripe/v1 JavaBindings/%s", Stripe.VERSION));
String userAgent = HttpClient.buildUserAgentString(request);
assertTrue(userAgent.startsWith(String.format("Stripe/v1 JavaBindings/%s", Stripe.VERSION)));
}

@Test
Expand All @@ -207,8 +206,52 @@ public void testV2RequestSetsCorrectUserAgent() throws StripeException {
RequestOptions.builder().setApiKey("sk_test_123").setMaxNetworkRetries(2).build(),
ApiMode.V2);

assertEquals(
HttpClient.buildUserAgentString(request),
String.format("Stripe/v2 JavaBindings/%s", Stripe.VERSION));
String userAgent = HttpClient.buildUserAgentString(request);
assertTrue(userAgent.startsWith(String.format("Stripe/v2 JavaBindings/%s", Stripe.VERSION)));
}

@Test
public void testDetectAIAgent() {
String agent = HttpClient.detectAIAgent(key -> key.equals("CLAUDECODE") ? "1" : null);
assertEquals("claude_code", agent);
}

@Test
public void testDetectAIAgentNoEnv() {
String agent = HttpClient.detectAIAgent(key -> null);
assertEquals("", agent);
}

@Test
public void testDetectAIAgentFirstMatchWins() {
String agent =
HttpClient.detectAIAgent(
key -> {
if (key.equals("CURSOR_AGENT") || key.equals("OPENCODE")) return "1";
return null;
});
assertEquals("cursor", agent);
}

@Test
public void testBuildUserAgentStringWithAIAgent() throws StripeException {
StripeRequest request =
StripeRequest.create(
ApiResource.RequestMethod.GET,
"http://example.com/get",
null,
RequestOptions.builder().setApiKey("sk_test_123").build(),
ApiMode.V1);

String userAgent = HttpClient.buildUserAgentString(request, "cursor");
assertTrue(userAgent.contains("AIAgent/cursor"));
}

@Test
public void testBuildXStripeClientUserAgentStringWithAIAgent() {
String json = HttpClient.buildXStripeClientUserAgentString("cursor");
com.google.gson.JsonObject parsed =
com.google.gson.JsonParser.parseString(json).getAsJsonObject();
assertEquals("cursor", parsed.get("ai_agent").getAsString());
}
}