Skip to content

Commit 80c6664

Browse files
committed
Added changes to handle EmptyString and Empty list to add SDK User Agent
1 parent e8b1e16 commit 80c6664

File tree

3 files changed

+146
-50
lines changed

3 files changed

+146
-50
lines changed

core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/ApplyUserAgentStage.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ public ApplyUserAgentStage(HttpClientDependencies dependencies) {
6868
public SdkHttpFullRequest.Builder execute(SdkHttpFullRequest.Builder request,
6969
RequestExecutionContext context) throws Exception {
7070

71-
if (hasNonNullUserAgentHeader(request)) {
71+
if (request.firstMatchingHeader(HEADER_USER_AGENT).isPresent()) {
7272
return request;
7373
}
7474
String headerValue = finalizeUserAgent(context);

core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/pipeline/stages/ApplyUserAgentStageTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ public void when_userAgentHeaderPresentButEmpty_EmptyHeaderIsPreserved() throws
182182

183183
List<String> userAgentHeaders = result.headers().get(HEADER_USER_AGENT);
184184
assertThat(userAgentHeaders).isNotNull().hasSize(1);
185-
assertThat(userAgentHeaders.get(0)).doesNotContain("aws-sdk-java");
185+
assertThat(userAgentHeaders.get(0)).contains("aws-sdk-java");
186186
}
187187

188188
@Test

test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/useragent/CustomUserAgentHeaderTest.java

Lines changed: 144 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
import static org.assertj.core.api.Assertions.assertThat;
1919
import static org.assertj.core.api.Assertions.assertThatThrownBy;
2020

21+
import java.util.Arrays;
22+
import java.util.Collections;
2123
import java.util.List;
2224
import java.util.Map;
2325
import java.util.stream.Stream;
@@ -34,48 +36,63 @@
3436
import software.amazon.awssdk.regions.Region;
3537
import software.amazon.awssdk.services.restjsonendpointproviders.RestJsonEndpointProvidersClient;
3638
import software.amazon.awssdk.services.restjsonendpointproviders.RestJsonEndpointProvidersClientBuilder;
39+
import software.amazon.awssdk.utils.StringUtils;
3740

3841
/**
39-
* Functional tests to verify that custom User-Agent headers provided via
40-
* {@link software.amazon.awssdk.core.client.config.ClientOverrideConfiguration.Builder#putHeader(String, String)}
41-
* are preserved and not overwritten by the SDK's default User-Agent generation logic.
42+
* Functional tests verifying custom User-Agent header preservation.
43+
*
44+
* <p>Tests ensure that User-Agent headers provided via
45+
* {@link software.amazon.awssdk.core.client.config.ClientOverrideConfiguration.Builder#putHeader(String, String)} are preserved
46+
* and not overwritten by SDK's default User-Agent generation logic.
4247
*/
4348
class CustomUserAgentHeaderTest {
4449

4550
private static final String USER_AGENT_HEADER = "User-Agent";
4651
private static final String SDK_USER_AGENT_PREFIX = "aws-sdk-java";
4752
private static final String TEST_API_NAME = "TestApiName";
4853
private static final String TEST_API_VERSION = "1.0";
54+
private static final String INTERCEPTOR_STOP_MESSAGE = "stop";
4955

5056
private CapturingInterceptor interceptor;
5157

58+
private static Stream<Arguments> customUserAgentValues() {
59+
return Stream.of(
60+
Arguments.of("CustomUserAgentHeaderValue"),
61+
Arguments.of("MyApplication/1.0.0"),
62+
Arguments.of("CustomClient/2.0 (Linux; x86_64)")
63+
);
64+
}
65+
66+
// ========== Default Behavior Tests ==========
67+
68+
private static Stream<Arguments> customUserAgentListValues() {
69+
return Stream.of(
70+
Arguments.of(Arrays.asList("Agent1")),
71+
Arguments.of(Arrays.asList("Agent1", "Agent2")),
72+
Arguments.of(Arrays.asList("CustomClient/1.0", "MyApp/2.0"))
73+
);
74+
}
75+
76+
// ========== Custom User-Agent Preservation Tests ==========
77+
5278
@BeforeEach
5379
void setUp() {
5480
interceptor = new CapturingInterceptor();
5581
}
5682

5783
@Test
58-
void execute_withoutCustomUserAgent_shouldAddSdkDefaultUserAgent() {
59-
84+
void executeRequest_withoutCustomUserAgent_shouldAddSdkDefaultUserAgent() {
6085
RestJsonEndpointProvidersClient client = defaultClientBuilder().build();
6186
executeRequestExpectingInterception(client);
62-
String userAgent = getCapturedUserAgent();
63-
assertThat(userAgent).contains(SDK_USER_AGENT_PREFIX);
64-
}
65-
66-
@Test
67-
void execute_withEmptyCustomUserAgent_shouldPreserveEmptyValue() {
6887

69-
RestJsonEndpointProvidersClient client = clientWithCustomUserAgent("");
70-
executeRequestExpectingInterception(client);
71-
String userAgent = getCapturedUserAgent();
72-
assertThat(userAgent).isEmpty();
88+
assertUserAgentContains(SDK_USER_AGENT_PREFIX);
7389
}
7490

75-
@ParameterizedTest(name = "{index}: userAgent={0}")
76-
@MethodSource("customUserAgentValues")
77-
void execute_withCustomUserAgent_shouldPreserveAndNotOverwrite(String customUserAgent) {
91+
// ========== API Name Handling Tests ==========
7892

93+
@ParameterizedTest(name = "Custom User-Agent ''{0}'' should be preserved without SDK prefix")
94+
@MethodSource("customUserAgentValues")
95+
void executeRequest_withCustomUserAgent_shouldPreserveAndNotOverwrite(String customUserAgent) {
7996
RestJsonEndpointProvidersClient client = clientWithCustomUserAgent(customUserAgent);
8097
executeRequestExpectingInterception(client);
8198

@@ -85,33 +102,120 @@ void execute_withCustomUserAgent_shouldPreserveAndNotOverwrite(String customUser
85102
.doesNotContain(SDK_USER_AGENT_PREFIX);
86103
}
87104

88-
private static Stream<Arguments> customUserAgentValues() {
89-
return Stream.of(
90-
Arguments.of("CustomUserAgentHeaderValue"),
91-
Arguments.of("MyApplication/1.0.0"),
92-
Arguments.of("CustomClient/2.0 (Linux; x86_64)")
93-
);
105+
@ParameterizedTest(name = "Custom User-Agent list {0} should be preserved")
106+
@MethodSource("customUserAgentListValues")
107+
void executeRequest_withCustomUserAgentList_shouldPreserveAllValues(List<String> customUserAgentList) {
108+
RestJsonEndpointProvidersClient client = clientWithCustomUserAgentList(customUserAgentList);
109+
executeRequestExpectingInterception(client);
110+
111+
List<String> userAgentList = getCapturedUserAgentList();
112+
assertThat(userAgentList).isEqualTo(customUserAgentList);
94113
}
95114

96-
@Test
97-
void execute_withCustomUserAgentAndApiName_shouldNotAppendApiName() {
115+
// ========== Edge Case Tests ==========
98116

117+
@Test
118+
void executeRequest_withCustomUserAgentAndApiName_shouldNotAppendApiName() {
99119
String customUserAgent = "CustomUserAgentHeaderValue";
100120
RestJsonEndpointProvidersClient client = clientWithCustomUserAgent(customUserAgent);
101121
executeRequestWithApiName(client);
122+
102123
String userAgent = getCapturedUserAgent();
103124
assertThat(userAgent)
104125
.isEqualTo(customUserAgent)
105126
.doesNotContain(TEST_API_NAME);
106127
}
107128

108129
@Test
109-
void execute_withoutCustomUserAgentAndWithApiName_shouldAppendApiName() {
110-
130+
void executeRequest_withoutCustomUserAgentAndWithApiName_shouldAppendApiName() {
111131
RestJsonEndpointProvidersClient client = defaultClientBuilder().build();
112132
executeRequestWithApiName(client);
113-
String userAgent = getCapturedUserAgent();
114-
assertThat(userAgent).contains(TEST_API_NAME + "/" + TEST_API_VERSION);
133+
134+
assertUserAgentContains(TEST_API_NAME + "/" + TEST_API_VERSION);
135+
}
136+
137+
/**
138+
* Verifies that null User-Agent list throws NullPointerException.
139+
*
140+
* <p>This ensures the SDK fails fast with clear error rather than allowing
141+
* invalid configuration.
142+
*/
143+
@Test
144+
void buildClient_withNullListUserAgent_shouldThrowNullPointerException() {
145+
assertThatThrownBy(() -> clientWithCustomUserAgentList(null))
146+
.isInstanceOf(NullPointerException.class)
147+
.hasMessageContaining("values must not be null");
148+
}
149+
150+
/**
151+
* Verifies that empty User-Agent list results in SDK default User-Agent.
152+
*
153+
* <p><b>Behavioral Change:</b> Previously as in when UserAgentApplyStage was done before MergeCustomHeaderStage, explicitly
154+
* setting User-Agent Header to empty String or empty list would delete the SDK User-Agent. Current behavior ensures SDK
155+
* User-Agent is always present when User-Agent Header is emptyString/EmptyList/Null.
156+
*/
157+
@Test
158+
void executeRequest_withEmptyListUserAgent_shouldResultInSdkUserAgentHeader() {
159+
RestJsonEndpointProvidersClient client = clientWithCustomUserAgentList(Collections.emptyList());
160+
executeRequestExpectingInterception(client);
161+
162+
List<String> userAgentList = getCapturedUserAgentList();
163+
assertThat(userAgentList)
164+
.hasSize(1)
165+
.anyMatch(ua -> ua.startsWith(SDK_USER_AGENT_PREFIX));
166+
}
167+
168+
@Test
169+
void executeRequest_withEmptyCustomUserAgent_shouldStoreSdkUserAgent() {
170+
RestJsonEndpointProvidersClient client = clientWithCustomUserAgent("");
171+
executeRequestExpectingInterception(client);
172+
173+
assertUserAgentContains(SDK_USER_AGENT_PREFIX);
174+
}
175+
176+
@Test
177+
void executeRequest_withNullStringUserAgent_shouldStoreAsSdkUserAgent() {
178+
RestJsonEndpointProvidersClient client = clientWithCustomUserAgent(null);
179+
executeRequestExpectingInterception(client);
180+
181+
List<String> userAgentList = getCapturedUserAgentList();
182+
assertThat(userAgentList).hasSize(1);
183+
assertThat(userAgentList)
184+
.hasSize(1)
185+
.allSatisfy(ua -> {
186+
assertThat(ua).isNotNull();
187+
assertThat(ua).startsWith(SDK_USER_AGENT_PREFIX);
188+
});
189+
190+
}
191+
192+
private void assertUserAgentContains(String expected) {
193+
assertThat(getCapturedUserAgent()).contains(expected);
194+
}
195+
196+
private void executeRequestExpectingInterception(RestJsonEndpointProvidersClient client) {
197+
assertThatThrownBy(() -> client.allTypes(r -> {
198+
}))
199+
.hasMessageContaining(INTERCEPTOR_STOP_MESSAGE);
200+
}
201+
202+
private void executeRequestWithApiName(RestJsonEndpointProvidersClient client) {
203+
assertThatThrownBy(() -> client.allTypes(r -> r
204+
.overrideConfiguration(o -> o.addApiName(api -> api
205+
.name(TEST_API_NAME)
206+
.version(TEST_API_VERSION)))))
207+
.hasMessageContaining(INTERCEPTOR_STOP_MESSAGE);
208+
}
209+
210+
private String getCapturedUserAgent() {
211+
Map<String, List<String>> headers = interceptor.context.httpRequest().headers();
212+
assertThat(headers).containsKey(USER_AGENT_HEADER);
213+
return headers.get(USER_AGENT_HEADER).get(0);
214+
}
215+
216+
private List<String> getCapturedUserAgentList() {
217+
Map<String, List<String>> headers = interceptor.context.httpRequest().headers();
218+
return headers.get(USER_AGENT_HEADER);
115219
}
116220

117221
private RestJsonEndpointProvidersClientBuilder defaultClientBuilder() {
@@ -133,23 +237,15 @@ private RestJsonEndpointProvidersClient clientWithCustomUserAgent(String customU
133237
.build();
134238
}
135239

136-
private void executeRequestExpectingInterception(RestJsonEndpointProvidersClient client) {
137-
assertThatThrownBy(() -> client.allTypes(r -> {}))
138-
.hasMessageContaining("stop");
139-
}
140-
141-
private void executeRequestWithApiName(RestJsonEndpointProvidersClient client) {
142-
assertThatThrownBy(() -> client.allTypes(r -> r
143-
.overrideConfiguration(o -> o.addApiName(api -> api
144-
.name(TEST_API_NAME)
145-
.version(TEST_API_VERSION)))))
146-
.hasMessageContaining("stop");
147-
}
148-
149-
private String getCapturedUserAgent() {
150-
Map<String, List<String>> headers = interceptor.context.httpRequest().headers();
151-
assertThat(headers).containsKey(USER_AGENT_HEADER);
152-
return headers.get(USER_AGENT_HEADER).get(0);
240+
private RestJsonEndpointProvidersClient clientWithCustomUserAgentList(List<String> customUserAgentList) {
241+
return RestJsonEndpointProvidersClient.builder()
242+
.region(Region.US_WEST_2)
243+
.credentialsProvider(StaticCredentialsProvider.create(
244+
AwsBasicCredentials.create("akid", "skid")))
245+
.overrideConfiguration(c -> c
246+
.addExecutionInterceptor(interceptor)
247+
.putHeader(USER_AGENT_HEADER, customUserAgentList))
248+
.build();
153249
}
154250

155251
private static class CapturingInterceptor implements ExecutionInterceptor {
@@ -158,7 +254,7 @@ private static class CapturingInterceptor implements ExecutionInterceptor {
158254
@Override
159255
public void beforeTransmission(Context.BeforeTransmission context, ExecutionAttributes executionAttributes) {
160256
this.context = context;
161-
throw new RuntimeException("stop");
257+
throw new RuntimeException(INTERCEPTOR_STOP_MESSAGE);
162258
}
163259
}
164260
}

0 commit comments

Comments
 (0)