From 2e2c6ce65b52d0603845b661c718e7d1723e22e3 Mon Sep 17 00:00:00 2001 From: David Brownman Date: Tue, 2 Jun 2026 17:09:36 -0700 Subject: [PATCH] Add source field to user-agent header --- src/main/java/com/stripe/net/HttpClient.java | 52 +++++++++++++++++++ .../java/com/stripe/net/HttpClientTest.java | 28 ++++++++++ 2 files changed, 80 insertions(+) diff --git a/src/main/java/com/stripe/net/HttpClient.java b/src/main/java/com/stripe/net/HttpClient.java index 8aa3fcda5f1..9d21f438342 100644 --- a/src/main/java/com/stripe/net/HttpClient.java +++ b/src/main/java/com/stripe/net/HttpClient.java @@ -7,6 +7,7 @@ import java.io.InputStream; import java.net.ConnectException; import java.net.SocketTimeoutException; +import java.security.MessageDigest; import java.time.Duration; import java.util.HashMap; import java.util.Map; @@ -26,6 +27,53 @@ public abstract class HttpClient { /** A value indicating whether the client should sleep between automatic request retries. */ boolean networkRetriesSleep = true; + static String UNAME_HASH = computeUnameHash(); + + private static String computeUnameHash() { + String uname = ""; + try { + uname = + (System.getProperty("os.name", "") + + " " + + System.getProperty("os.version", "") + + " " + + System.getProperty("os.arch", "") + + " " + + System.getProperty("java.version", "") + + " " + + System.getProperty("java.vendor", "") + + " " + + System.getProperty("java.vm.name", "") + + " " + + getHostname()) + .trim(); + } catch (Exception e) { + // fall through with empty string + } + if (uname.isEmpty()) { + return ""; + } + try { + MessageDigest md = MessageDigest.getInstance("MD5"); + byte[] hashBytes = md.digest(uname.getBytes(java.nio.charset.StandardCharsets.UTF_8)); + StringBuilder sb = new StringBuilder(); + for (byte b : hashBytes) { + sb.append(String.format("%02x", b)); + } + return sb.toString(); + } catch (Exception e) { + return ""; + } + } + + private static String getHostname() { + try { + return java.net.InetAddress.getLocalHost().getHostName(); + } catch (Exception e) { + return ""; + } + } + /** Initializes a new instance of the {@link HttpClient} class. */ protected HttpClient() {} @@ -228,6 +276,10 @@ static String buildXStripeClientUserAgentString(String aiAgent) { propertyMap.put("ai_agent", aiAgent); } + if (!UNAME_HASH.isEmpty()) { + propertyMap.put("source", UNAME_HASH); + } + return ApiResource.INTERNAL_GSON.toJson(propertyMap); } diff --git a/src/test/java/com/stripe/net/HttpClientTest.java b/src/test/java/com/stripe/net/HttpClientTest.java index aa0d1f0ebc9..14bfbfcdd40 100644 --- a/src/test/java/com/stripe/net/HttpClientTest.java +++ b/src/test/java/com/stripe/net/HttpClientTest.java @@ -1,6 +1,7 @@ package com.stripe.net; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -295,4 +296,31 @@ public void testBuildXStripeClientUserAgentStringNoPlatformWithoutTelemetry() { Stripe.enableTelemetry = originalTelemetry; } } + + @Test + public void testBuildXStripeClientUserAgentStringIncludesSource() { + String json = HttpClient.buildXStripeClientUserAgentString(""); + com.google.gson.JsonObject parsed = + com.google.gson.JsonParser.parseString(json).getAsJsonObject(); + // "source" should be present and be a 32-character lowercase MD5 hex digest + assertTrue(parsed.has("source"), "Expected 'source' field in X-Stripe-Client-User-Agent"); + String source = parsed.get("source").getAsString(); + assertTrue( + source.matches("[0-9a-f]{32}"), + "Expected 'source' to be a 32-character lowercase hex string, got: " + source); + } + + @Test + public void testBuildXStripeClientUserAgentStringOmitsSourceWhenEmpty() throws Exception { + String savedHash = HttpClient.UNAME_HASH; + try { + HttpClient.UNAME_HASH = ""; + String userAgentString = HttpClient.buildXStripeClientUserAgentString(""); + com.google.gson.JsonObject userAgent = + com.google.gson.JsonParser.parseString(userAgentString).getAsJsonObject(); + assertFalse(userAgent.has("source")); + } finally { + HttpClient.UNAME_HASH = savedHash; + } + } }