Skip to content

Commit e8b1e16

Browse files
committed
Skip modifying User-Agent in ApplyUserAgentStage if the user has already passed custom User-Agent in the request
1 parent 20b2376 commit e8b1e16

File tree

4 files changed

+263
-1
lines changed

4 files changed

+263
-1
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"type": "bugfix",
3+
"category": "AWS SDK for Java v2",
4+
"contributor": "",
5+
"description": "Skip User-Agent header modification in ApplyUserAgentStage when custom User-Agent is already provided."
6+
}

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import software.amazon.awssdk.core.useragent.BusinessMetricCollection;
4141
import software.amazon.awssdk.http.SdkHttpFullRequest;
4242
import software.amazon.awssdk.identity.spi.Identity;
43+
import software.amazon.awssdk.utils.CollectionUtils;
4344
import software.amazon.awssdk.utils.CompletableFutureUtils;
4445
import software.amazon.awssdk.utils.Logger;
4546
import software.amazon.awssdk.utils.Pair;
@@ -66,10 +67,23 @@ public ApplyUserAgentStage(HttpClientDependencies dependencies) {
6667
@Override
6768
public SdkHttpFullRequest.Builder execute(SdkHttpFullRequest.Builder request,
6869
RequestExecutionContext context) throws Exception {
70+
71+
if (hasNonNullUserAgentHeader(request)) {
72+
return request;
73+
}
6974
String headerValue = finalizeUserAgent(context);
7075
return request.putHeader(HEADER_USER_AGENT, headerValue);
7176
}
7277

78+
/**
79+
* Checks if User-Agent header exists with a non-null value (including empty string).
80+
* This is done to maintain backward compatibility since MergeCustomHeadersStage merges non-null headers only.
81+
*/
82+
private boolean hasNonNullUserAgentHeader(SdkHttpFullRequest.Builder request) {
83+
List<String> userAgentValues = request.matchingHeaders(HEADER_USER_AGENT);
84+
return CollectionUtils.firstIfPresent(userAgentValues) != null;
85+
}
86+
7387
/**
7488
* The final value sent in the user agent header consists of
7589
* <ol>

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

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,85 @@ public void when_identityContainsProvider_authSourceIsPresent() throws Exception
152152
assertThat(userAgentHeaders.get(0)).contains("m/w");
153153
}
154154

155+
@Test
156+
public void when_userAgentHeaderAlreadyPresent_doesNotOverwrite() throws Exception {
157+
ApplyUserAgentStage stage = new ApplyUserAgentStage(dependencies(clientUserAgent()));
158+
159+
String existingUserAgent = "CustomUserAgent/1.0";
160+
SdkHttpFullRequest.Builder requestWithExistingHeader = SdkHttpFullRequest.builder()
161+
.putHeader(HEADER_USER_AGENT, existingUserAgent);
162+
163+
RequestExecutionContext ctx = requestExecutionContext(executionAttributes(IDENTITY_WITHOUT_SOURCE), noOpRequest());
164+
SdkHttpFullRequest.Builder result = stage.execute(requestWithExistingHeader, ctx);
165+
166+
List<String> userAgentHeaders = result.headers().get(HEADER_USER_AGENT);
167+
assertThat(userAgentHeaders).isNotNull().hasSize(1);
168+
assertThat(userAgentHeaders.get(0)).isEqualTo(existingUserAgent);
169+
// Verify it does NOT contain SDK user agent values
170+
assertThat(userAgentHeaders.get(0)).doesNotContain("aws-sdk-java");
171+
}
172+
173+
@Test
174+
public void when_userAgentHeaderPresentButEmpty_EmptyHeaderIsPreserved() throws Exception {
175+
ApplyUserAgentStage stage = new ApplyUserAgentStage(dependencies(clientUserAgent()));
176+
177+
SdkHttpFullRequest.Builder requestWithEmptyHeader = SdkHttpFullRequest.builder()
178+
.putHeader(HEADER_USER_AGENT, "");
179+
180+
RequestExecutionContext ctx = requestExecutionContext(executionAttributes(IDENTITY_WITHOUT_SOURCE), noOpRequest());
181+
SdkHttpFullRequest.Builder result = stage.execute(requestWithEmptyHeader, ctx);
182+
183+
List<String> userAgentHeaders = result.headers().get(HEADER_USER_AGENT);
184+
assertThat(userAgentHeaders).isNotNull().hasSize(1);
185+
assertThat(userAgentHeaders.get(0)).doesNotContain("aws-sdk-java");
186+
}
187+
188+
@Test
189+
public void when_userAgentHeaderPresentButNull_sdkAddsHeader() throws Exception {
190+
ApplyUserAgentStage stage = new ApplyUserAgentStage(dependencies(clientUserAgent()));
191+
String headerValue = null;
192+
SdkHttpFullRequest.Builder requestWithEmptyHeader = SdkHttpFullRequest.builder()
193+
.putHeader(HEADER_USER_AGENT, headerValue);
194+
195+
RequestExecutionContext ctx = requestExecutionContext(executionAttributes(IDENTITY_WITHOUT_SOURCE), noOpRequest());
196+
SdkHttpFullRequest.Builder result = stage.execute(requestWithEmptyHeader, ctx);
197+
198+
List<String> userAgentHeaders = result.headers().get(HEADER_USER_AGENT);
199+
assertThat(userAgentHeaders).isNotNull().hasSize(1);
200+
assertThat(userAgentHeaders.get(0)).startsWith("aws-sdk-java");
201+
}
202+
203+
@Test
204+
public void when_userAgentHeaderAbsent_addsHeader() throws Exception {
205+
ApplyUserAgentStage stage = new ApplyUserAgentStage(dependencies(clientUserAgent()));
206+
207+
SdkHttpFullRequest.Builder requestWithoutHeader = SdkHttpFullRequest.builder();
208+
209+
RequestExecutionContext ctx = requestExecutionContext(executionAttributes(IDENTITY_WITHOUT_SOURCE), noOpRequest());
210+
SdkHttpFullRequest.Builder result = stage.execute(requestWithoutHeader, ctx);
211+
212+
List<String> userAgentHeaders = result.headers().get(HEADER_USER_AGENT);
213+
assertThat(userAgentHeaders).isNotNull().hasSize(1);
214+
assertThat(userAgentHeaders.get(0)).startsWith("aws-sdk-java");
215+
}
216+
217+
@Test
218+
public void when_multipleUserAgentHeadersPresent_doesNotOverwrite() throws Exception {
219+
ApplyUserAgentStage stage = new ApplyUserAgentStage(dependencies(clientUserAgent()));
220+
221+
SdkHttpFullRequest.Builder requestWithMultipleHeaders =
222+
SdkHttpFullRequest.builder()
223+
.putHeader(HEADER_USER_AGENT, "CustomAgent/1.0")
224+
.appendHeader(HEADER_USER_AGENT, "AnotherAgent/2.0");
225+
226+
RequestExecutionContext ctx = requestExecutionContext(executionAttributes(IDENTITY_WITHOUT_SOURCE), noOpRequest());
227+
SdkHttpFullRequest.Builder result = stage.execute(requestWithMultipleHeaders, ctx);
228+
229+
List<String> userAgentHeaders = result.headers().get(HEADER_USER_AGENT);
230+
assertThat(userAgentHeaders).hasSize(2);
231+
assertThat(userAgentHeaders).containsExactly("CustomAgent/1.0", "AnotherAgent/2.0");
232+
}
233+
155234
private static HttpClientDependencies dependencies(String clientUserAgent) {
156235
return dependencies(clientUserAgent, null, null);
157236
}
@@ -219,6 +298,5 @@ private RequestExecutionContext requestExecutionContext(ExecutionAttributes exec
219298
return RequestExecutionContext.builder()
220299
.executionContext(executionContext)
221300
.originalRequest(request).build();
222-
223301
}
224302
}
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.awssdk.services.useragent;
17+
18+
import static org.assertj.core.api.Assertions.assertThat;
19+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
20+
21+
import java.util.List;
22+
import java.util.Map;
23+
import java.util.stream.Stream;
24+
import org.junit.jupiter.api.BeforeEach;
25+
import org.junit.jupiter.api.Test;
26+
import org.junit.jupiter.params.ParameterizedTest;
27+
import org.junit.jupiter.params.provider.Arguments;
28+
import org.junit.jupiter.params.provider.MethodSource;
29+
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
30+
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
31+
import software.amazon.awssdk.core.interceptor.Context;
32+
import software.amazon.awssdk.core.interceptor.ExecutionAttributes;
33+
import software.amazon.awssdk.core.interceptor.ExecutionInterceptor;
34+
import software.amazon.awssdk.regions.Region;
35+
import software.amazon.awssdk.services.restjsonendpointproviders.RestJsonEndpointProvidersClient;
36+
import software.amazon.awssdk.services.restjsonendpointproviders.RestJsonEndpointProvidersClientBuilder;
37+
38+
/**
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+
*/
43+
class CustomUserAgentHeaderTest {
44+
45+
private static final String USER_AGENT_HEADER = "User-Agent";
46+
private static final String SDK_USER_AGENT_PREFIX = "aws-sdk-java";
47+
private static final String TEST_API_NAME = "TestApiName";
48+
private static final String TEST_API_VERSION = "1.0";
49+
50+
private CapturingInterceptor interceptor;
51+
52+
@BeforeEach
53+
void setUp() {
54+
interceptor = new CapturingInterceptor();
55+
}
56+
57+
@Test
58+
void execute_withoutCustomUserAgent_shouldAddSdkDefaultUserAgent() {
59+
60+
RestJsonEndpointProvidersClient client = defaultClientBuilder().build();
61+
executeRequestExpectingInterception(client);
62+
String userAgent = getCapturedUserAgent();
63+
assertThat(userAgent).contains(SDK_USER_AGENT_PREFIX);
64+
}
65+
66+
@Test
67+
void execute_withEmptyCustomUserAgent_shouldPreserveEmptyValue() {
68+
69+
RestJsonEndpointProvidersClient client = clientWithCustomUserAgent("");
70+
executeRequestExpectingInterception(client);
71+
String userAgent = getCapturedUserAgent();
72+
assertThat(userAgent).isEmpty();
73+
}
74+
75+
@ParameterizedTest(name = "{index}: userAgent={0}")
76+
@MethodSource("customUserAgentValues")
77+
void execute_withCustomUserAgent_shouldPreserveAndNotOverwrite(String customUserAgent) {
78+
79+
RestJsonEndpointProvidersClient client = clientWithCustomUserAgent(customUserAgent);
80+
executeRequestExpectingInterception(client);
81+
82+
String userAgent = getCapturedUserAgent();
83+
assertThat(userAgent)
84+
.isEqualTo(customUserAgent)
85+
.doesNotContain(SDK_USER_AGENT_PREFIX);
86+
}
87+
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+
);
94+
}
95+
96+
@Test
97+
void execute_withCustomUserAgentAndApiName_shouldNotAppendApiName() {
98+
99+
String customUserAgent = "CustomUserAgentHeaderValue";
100+
RestJsonEndpointProvidersClient client = clientWithCustomUserAgent(customUserAgent);
101+
executeRequestWithApiName(client);
102+
String userAgent = getCapturedUserAgent();
103+
assertThat(userAgent)
104+
.isEqualTo(customUserAgent)
105+
.doesNotContain(TEST_API_NAME);
106+
}
107+
108+
@Test
109+
void execute_withoutCustomUserAgentAndWithApiName_shouldAppendApiName() {
110+
111+
RestJsonEndpointProvidersClient client = defaultClientBuilder().build();
112+
executeRequestWithApiName(client);
113+
String userAgent = getCapturedUserAgent();
114+
assertThat(userAgent).contains(TEST_API_NAME + "/" + TEST_API_VERSION);
115+
}
116+
117+
private RestJsonEndpointProvidersClientBuilder defaultClientBuilder() {
118+
return RestJsonEndpointProvidersClient.builder()
119+
.region(Region.US_WEST_2)
120+
.credentialsProvider(StaticCredentialsProvider.create(
121+
AwsBasicCredentials.create("akid", "skid")))
122+
.overrideConfiguration(c -> c.addExecutionInterceptor(interceptor));
123+
}
124+
125+
private RestJsonEndpointProvidersClient clientWithCustomUserAgent(String customUserAgent) {
126+
return RestJsonEndpointProvidersClient.builder()
127+
.region(Region.US_WEST_2)
128+
.credentialsProvider(StaticCredentialsProvider.create(
129+
AwsBasicCredentials.create("akid", "skid")))
130+
.overrideConfiguration(c -> c
131+
.addExecutionInterceptor(interceptor)
132+
.putHeader(USER_AGENT_HEADER, customUserAgent))
133+
.build();
134+
}
135+
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);
153+
}
154+
155+
private static class CapturingInterceptor implements ExecutionInterceptor {
156+
private Context.BeforeTransmission context;
157+
158+
@Override
159+
public void beforeTransmission(Context.BeforeTransmission context, ExecutionAttributes executionAttributes) {
160+
this.context = context;
161+
throw new RuntimeException("stop");
162+
}
163+
}
164+
}

0 commit comments

Comments
 (0)