diff --git a/.changes/next-release/feature-AWSSDKforJavav2-83dd6ab.json b/.changes/next-release/feature-AWSSDKforJavav2-83dd6ab.json new file mode 100644 index 00000000000..4945e59bcc0 --- /dev/null +++ b/.changes/next-release/feature-AWSSDKforJavav2-83dd6ab.json @@ -0,0 +1,6 @@ +{ + "type": "feature", + "category": "AWS SDK for Java v2", + "contributor": "", + "description": "Add HTTP client configuration type metadata to the User-Agent header, tracking whether the HTTP client was auto-detected from the classpath or explicitly configured by the user." +} diff --git a/core/aws-core/src/main/java/software/amazon/awssdk/awscore/internal/AwsExecutionContextBuilder.java b/core/aws-core/src/main/java/software/amazon/awssdk/awscore/internal/AwsExecutionContextBuilder.java index 32273f16019..ac661d1a217 100644 --- a/core/aws-core/src/main/java/software/amazon/awssdk/awscore/internal/AwsExecutionContextBuilder.java +++ b/core/aws-core/src/main/java/software/amazon/awssdk/awscore/internal/AwsExecutionContextBuilder.java @@ -171,6 +171,7 @@ private AwsExecutionContextBuilder() { AwsSignerExecutionAttribute.AWS_CREDENTIALS).orElse(null))); putStreamingInputOutputTypesMetadata(executionAttributes, executionParams); + putHttpClientConfigTypeMetadata(executionAttributes, clientConfig); return ExecutionContext.builder() .interceptorChain(executionInterceptorChain) @@ -183,53 +184,54 @@ private AwsExecutionContextBuilder() { private static void putStreamingInputOutputTypesMetadata( ExecutionAttributes executionAttributes, ClientExecutionParams executionParams) { - List userAgentMetadata = new ArrayList<>(); if (executionParams.getRequestBody() != null) { - userAgentMetadata.add( - AdditionalMetadata - .builder() - .name("rb") - .value(ContentStreamProvider.ProviderType.shortValueFromName( - executionParams.getRequestBody().contentStreamProvider().name()) - ) - .build()); + addUserAgentMetadata(executionAttributes, "rb", + ContentStreamProvider.ProviderType.shortValueFromName( + executionParams.getRequestBody().contentStreamProvider().name())); } if (executionParams.getAsyncRequestBody() != null) { - userAgentMetadata.add( - AdditionalMetadata - .builder() - .name("rb") - .value(AsyncRequestBody.BodyType.shortValueFromName( - executionParams.getAsyncRequestBody().body()) - ) - .build()); + addUserAgentMetadata(executionAttributes, "rb", + AsyncRequestBody.BodyType.shortValueFromName( + executionParams.getAsyncRequestBody().body())); } if (executionParams.getResponseTransformer() != null) { - userAgentMetadata.add( - AdditionalMetadata - .builder() - .name("rt") - .value(ResponseTransformer.TransformerType.shortValueFromName( - executionParams.getResponseTransformer().name()) - ) - .build()); + addUserAgentMetadata(executionAttributes, "rt", + ResponseTransformer.TransformerType.shortValueFromName( + executionParams.getResponseTransformer().name())); } if (executionParams.getAsyncResponseTransformer() != null) { - userAgentMetadata.add( - AdditionalMetadata - .builder() - .name("rt") - .value(AsyncResponseTransformer.TransformerType.shortValueFromName( - executionParams.getAsyncResponseTransformer().name()) - ) - .build()); + addUserAgentMetadata(executionAttributes, "rt", + AsyncResponseTransformer.TransformerType.shortValueFromName( + executionParams.getAsyncResponseTransformer().name())); } + } - executionAttributes.putAttribute(SdkInternalExecutionAttribute.USER_AGENT_METADATA, userAgentMetadata); + private static void putHttpClientConfigTypeMetadata(ExecutionAttributes executionAttributes, + SdkClientConfiguration clientConfig) { + String httpClientConfigType = clientConfig.option(SdkClientOption.HTTP_CLIENT_CONFIG_TYPE); + if (httpClientConfigType == null) { + return; + } + addUserAgentMetadata(executionAttributes, "hc", httpClientConfigType); + } + + private static void addUserAgentMetadata(ExecutionAttributes executionAttributes, String name, String value) { + List metadata = executionAttributes.getAttribute( + SdkInternalExecutionAttribute.USER_AGENT_METADATA); + if (metadata == null) { + metadata = new ArrayList<>(); + executionAttributes.putAttribute(SdkInternalExecutionAttribute.USER_AGENT_METADATA, metadata); + } + metadata.add( + AdditionalMetadata + .builder() + .name(name) + .value(value) + .build()); } /** diff --git a/core/aws-core/src/test/java/software/amazon/awssdk/awscore/internal/AwsExecutionContextBuilderTest.java b/core/aws-core/src/test/java/software/amazon/awssdk/awscore/internal/AwsExecutionContextBuilderTest.java index e6ab211de5d..27098f5cdec 100644 --- a/core/aws-core/src/test/java/software/amazon/awssdk/awscore/internal/AwsExecutionContextBuilderTest.java +++ b/core/aws-core/src/test/java/software/amazon/awssdk/awscore/internal/AwsExecutionContextBuilderTest.java @@ -510,6 +510,128 @@ public void invokeInterceptorsAndCreateExecutionContext_withAsyncResponseTransfo ); } + @Test + public void invokeInterceptorsAndCreateExecutionContext_withDefaultHttpClient_addsHcMetadata() { + SdkClientConfiguration clientConfig = testClientConfiguration() + .option(SdkClientOption.HTTP_CLIENT_CONFIG_TYPE, "d") + .build(); + + ExecutionContext executionContext = + AwsExecutionContextBuilder.invokeInterceptorsAndCreateExecutionContext(clientExecutionParams(), clientConfig); + + ExecutionAttributes executionAttributes = executionContext.executionAttributes(); + assertThat(executionAttributes.getAttribute(SdkInternalExecutionAttribute.USER_AGENT_METADATA)).isEqualTo( + Collections.singletonList(AdditionalMetadata.builder().name("hc").value("d").build()) + ); + } + + @Test + public void invokeInterceptorsAndCreateExecutionContext_withExplicitHttpClient_addsHcMetadata() { + SdkClientConfiguration clientConfig = testClientConfiguration() + .option(SdkClientOption.HTTP_CLIENT_CONFIG_TYPE, "e") + .build(); + + ExecutionContext executionContext = + AwsExecutionContextBuilder.invokeInterceptorsAndCreateExecutionContext(clientExecutionParams(), clientConfig); + + ExecutionAttributes executionAttributes = executionContext.executionAttributes(); + assertThat(executionAttributes.getAttribute(SdkInternalExecutionAttribute.USER_AGENT_METADATA)).isEqualTo( + Collections.singletonList(AdditionalMetadata.builder().name("hc").value("e").build()) + ); + } + + @Test + public void invokeInterceptorsAndCreateExecutionContext_withRequestBodyAndHcMetadata_addsBoth() throws IOException { + ClientExecutionParams executionParams = clientExecutionParams(); + File testFile = File.createTempFile("testFile", UUID.randomUUID().toString()); + testFile.deleteOnExit(); + executionParams.withRequestBody(RequestBody.fromFile(testFile)); + + SdkClientConfiguration clientConfig = testClientConfiguration() + .option(SdkClientOption.HTTP_CLIENT_CONFIG_TYPE, "d") + .build(); + + ExecutionContext executionContext = + AwsExecutionContextBuilder.invokeInterceptorsAndCreateExecutionContext(executionParams, clientConfig); + + ExecutionAttributes executionAttributes = executionContext.executionAttributes(); + assertThat(executionAttributes.getAttribute(SdkInternalExecutionAttribute.USER_AGENT_METADATA)).isEqualTo( + Arrays.asList( + AdditionalMetadata.builder().name("rb").value("f").build(), + AdditionalMetadata.builder().name("hc").value("d").build() + ) + ); + } + + @Test + public void invokeInterceptorsAndCreateExecutionContext_withAsyncRequestBodyAndHcMetadata_addsBoth() throws IOException { + ClientExecutionParams executionParams = clientExecutionParams(); + File testFile = File.createTempFile("testFile", UUID.randomUUID().toString()); + testFile.deleteOnExit(); + executionParams.withAsyncRequestBody(AsyncRequestBody.fromFile(testFile)); + + SdkClientConfiguration clientConfig = testClientConfiguration() + .option(SdkClientOption.HTTP_CLIENT_CONFIG_TYPE, "e") + .build(); + + ExecutionContext executionContext = + AwsExecutionContextBuilder.invokeInterceptorsAndCreateExecutionContext(executionParams, clientConfig); + + ExecutionAttributes executionAttributes = executionContext.executionAttributes(); + assertThat(executionAttributes.getAttribute(SdkInternalExecutionAttribute.USER_AGENT_METADATA)).isEqualTo( + Arrays.asList( + AdditionalMetadata.builder().name("rb").value("f").build(), + AdditionalMetadata.builder().name("hc").value("e").build() + ) + ); + } + + @Test + public void invokeInterceptorsAndCreateExecutionContext_withResponseTransformerAndHcMetadata_addsBoth() throws IOException { + ClientExecutionParams executionParams = clientExecutionParams(); + File testFile = File.createTempFile("testFile", UUID.randomUUID().toString()); + testFile.deleteOnExit(); + executionParams.withResponseTransformer(ResponseTransformer.toFile(testFile)); + + SdkClientConfiguration clientConfig = testClientConfiguration() + .option(SdkClientOption.HTTP_CLIENT_CONFIG_TYPE, "d") + .build(); + + ExecutionContext executionContext = + AwsExecutionContextBuilder.invokeInterceptorsAndCreateExecutionContext(executionParams, clientConfig); + + ExecutionAttributes executionAttributes = executionContext.executionAttributes(); + assertThat(executionAttributes.getAttribute(SdkInternalExecutionAttribute.USER_AGENT_METADATA)).isEqualTo( + Arrays.asList( + AdditionalMetadata.builder().name("rt").value("f").build(), + AdditionalMetadata.builder().name("hc").value("d").build() + ) + ); + } + + @Test + public void invokeInterceptorsAndCreateExecutionContext_withAsyncResponseTransformerAndHcMetadata_addsBoth() throws IOException { + ClientExecutionParams executionParams = clientExecutionParams(); + File testFile = File.createTempFile("testFile", UUID.randomUUID().toString()); + testFile.deleteOnExit(); + executionParams.withAsyncResponseTransformer(AsyncResponseTransformer.toFile(testFile)); + + SdkClientConfiguration clientConfig = testClientConfiguration() + .option(SdkClientOption.HTTP_CLIENT_CONFIG_TYPE, "e") + .build(); + + ExecutionContext executionContext = + AwsExecutionContextBuilder.invokeInterceptorsAndCreateExecutionContext(executionParams, clientConfig); + + ExecutionAttributes executionAttributes = executionContext.executionAttributes(); + assertThat(executionAttributes.getAttribute(SdkInternalExecutionAttribute.USER_AGENT_METADATA)).isEqualTo( + Arrays.asList( + AdditionalMetadata.builder().name("rt").value("f").build(), + AdditionalMetadata.builder().name("hc").value("e").build() + ) + ); + } + private ClientExecutionParams clientExecutionParams() { return clientExecutionParams(sdkRequest); } diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/builder/SdkDefaultClientBuilder.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/builder/SdkDefaultClientBuilder.java index 076f79b44c8..e5c0e7ee509 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/builder/SdkDefaultClientBuilder.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/builder/SdkDefaultClientBuilder.java @@ -39,6 +39,7 @@ import static software.amazon.awssdk.core.client.config.SdkClientOption.DEFAULT_RETRY_MODE; import static software.amazon.awssdk.core.client.config.SdkClientOption.EXECUTION_INTERCEPTORS; import static software.amazon.awssdk.core.client.config.SdkClientOption.HTTP_CLIENT_CONFIG; +import static software.amazon.awssdk.core.client.config.SdkClientOption.HTTP_CLIENT_CONFIG_TYPE; import static software.amazon.awssdk.core.client.config.SdkClientOption.IDENTITY_PROVIDERS; import static software.amazon.awssdk.core.client.config.SdkClientOption.INTERNAL_USER_AGENT; import static software.amazon.awssdk.core.client.config.SdkClientOption.METRIC_PUBLISHERS; @@ -310,34 +311,55 @@ protected SdkClientConfiguration finalizeChildConfiguration(SdkClientConfigurati */ private SdkClientConfiguration finalizeSyncConfiguration(SdkClientConfiguration config) { return config.toBuilder() + .option(HTTP_CLIENT_CONFIG_TYPE, resolveSyncHttpClientConfigType(config)) .lazyOption(SdkClientOption.SYNC_HTTP_CLIENT, c -> resolveSyncHttpClient(c, config)) .option(SdkClientOption.CLIENT_TYPE, SYNC) .build(); } + private String resolveSyncHttpClientConfigType(SdkClientConfiguration config) { + SdkHttpClient httpClient = config.option(CONFIGURED_SYNC_HTTP_CLIENT); + SdkHttpClient.Builder httpClientBuilder = config.option(CONFIGURED_SYNC_HTTP_CLIENT_BUILDER); + if (!(httpClient == null && httpClientBuilder == null)) { + return "e"; + } + return "d"; + } + /** * Finalize async-specific configuration from the default-applied configuration. */ private SdkClientConfiguration finalizeAsyncConfiguration(SdkClientConfiguration config) { return config.toBuilder() .lazyOptionIfAbsent(FUTURE_COMPLETION_EXECUTOR, this::resolveAsyncFutureCompletionExecutor) + .option(HTTP_CLIENT_CONFIG_TYPE, resolveAsyncHttpClientConfigType(config)) .lazyOption(ASYNC_HTTP_CLIENT, c -> resolveAsyncHttpClient(c, config)) .option(SdkClientOption.CLIENT_TYPE, ASYNC) .build(); } + private String resolveAsyncHttpClientConfigType(SdkClientConfiguration config) { + SdkAsyncHttpClient httpClient = config.option(CONFIGURED_ASYNC_HTTP_CLIENT); + SdkAsyncHttpClient.Builder httpClientBuilder = config.option(CONFIGURED_ASYNC_HTTP_CLIENT_BUILDER); + if (!(httpClient == null && httpClientBuilder == null)) { + return "e"; + } + return "d"; + } + /** * Finalize global configuration from the default-applied configuration. */ private SdkClientConfiguration finalizeConfiguration(SdkClientConfiguration config) { - return config.toBuilder() + SdkClientConfiguration.Builder builder = config.toBuilder() .lazyOption(SCHEDULED_EXECUTOR_SERVICE, this::resolveScheduledExecutorService) .lazyOptionIfAbsent(RETRY_STRATEGY, this::resolveRetryStrategy) .option(EXECUTION_INTERCEPTORS, resolveExecutionInterceptors(config)) .lazyOption(CLIENT_USER_AGENT, this::resolveClientUserAgent) .lazyOption(COMPRESSION_CONFIGURATION, this::resolveCompressionConfiguration) - .lazyOptionIfAbsent(IDENTITY_PROVIDERS, c -> IdentityProviders.builder().build()) - .build(); + .lazyOptionIfAbsent(IDENTITY_PROVIDERS, c -> IdentityProviders.builder().build()); + builder.computeOptionIfAbsent(HTTP_CLIENT_CONFIG_TYPE, () -> "d"); + return builder.build(); } private CompressionConfiguration resolveCompressionConfiguration(LazyValueSource config) { diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/SdkClientOption.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/SdkClientOption.java index e05753513ad..70997cf998f 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/SdkClientOption.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/SdkClientOption.java @@ -196,6 +196,18 @@ public final class SdkClientOption extends ClientOption { public static final SdkClientOption> CONFIGURED_SYNC_HTTP_CLIENT_BUILDER = new SdkClientOption<>(new UnsafeValueType(SdkAsyncHttpClient.Builder.class)); + /** + * The HTTP client configuration type indicating how the HTTP client was selected. + *

+ * Possible values: + *

    + *
  • {@code "d"} - Default: HTTP client was auto-detected from the classpath
  • + *
  • {@code "e"} - Explicit: HTTP client was explicitly configured by the user via + * {@code httpClient()} or {@code httpClientBuilder()} methods
  • + *
+ */ + public static final SdkClientOption HTTP_CLIENT_CONFIG_TYPE = new SdkClientOption<>(String.class); + /** * Configuration that should be used to build the {@link #SYNC_HTTP_CLIENT} or {@link #ASYNC_HTTP_CLIENT}. */ diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/client/builder/DefaultClientBuilderTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/client/builder/DefaultClientBuilderTest.java index 973fc5a92ce..118f26e1617 100644 --- a/core/sdk-core/src/test/java/software/amazon/awssdk/core/client/builder/DefaultClientBuilderTest.java +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/client/builder/DefaultClientBuilderTest.java @@ -32,6 +32,7 @@ import static software.amazon.awssdk.core.client.config.SdkClientOption.API_CALL_TIMEOUT; import static software.amazon.awssdk.core.client.config.SdkClientOption.EXECUTION_ATTRIBUTES; import static software.amazon.awssdk.core.client.config.SdkClientOption.EXECUTION_INTERCEPTORS; +import static software.amazon.awssdk.core.client.config.SdkClientOption.HTTP_CLIENT_CONFIG_TYPE; import static software.amazon.awssdk.core.client.config.SdkClientOption.METRIC_PUBLISHERS; import static software.amazon.awssdk.core.client.config.SdkClientOption.PROFILE_FILE; import static software.amazon.awssdk.core.client.config.SdkClientOption.PROFILE_FILE_SUPPLIER; @@ -87,8 +88,6 @@ import software.amazon.awssdk.metrics.MetricCollection; import software.amazon.awssdk.metrics.MetricPublisher; import software.amazon.awssdk.profiles.ProfileFile; -import software.amazon.awssdk.testutils.EnvironmentVariableHelper; -import software.amazon.awssdk.testutils.Waiter; import software.amazon.awssdk.utils.AttributeMap; import software.amazon.awssdk.utils.StringInputStream; @@ -389,6 +388,86 @@ public void explicitAsyncHttpClientProvided_ClientIsNotManagedBySdk() { verify(defaultAsyncHttpClientFactory, never()).buildWithDefaults(any()); } + @Test + public void noHttpClientProvided_httpClientConfigTypeIsDefault() { + TestClient client = testClientBuilder().build(); + assertThat(client.clientConfiguration.option(HTTP_CLIENT_CONFIG_TYPE)).isEqualTo("d"); + } + + @Test + public void noAsyncHttpClientProvided_httpClientConfigTypeIsDefault() { + TestAsyncClient client = testAsyncClientBuilder().build(); + assertThat(client.clientConfiguration.option(HTTP_CLIENT_CONFIG_TYPE)).isEqualTo("d"); + } + + @Test + public void explicitSyncHttpClientProvided_httpClientConfigTypeIsExplicit() { + TestClient client = testClientBuilder() + .httpClient(mock(SdkHttpClient.class)) + .build(); + assertThat(client.clientConfiguration.option(HTTP_CLIENT_CONFIG_TYPE)).isEqualTo("e"); + } + + @Test + public void explicitSyncHttpClientBuilderProvided_httpClientConfigTypeIsExplicit() { + TestClient client = testClientBuilder() + .httpClientBuilder((SdkHttpClient.Builder) serviceDefaults -> mock(SdkHttpClient.class)) + .build(); + assertThat(client.clientConfiguration.option(HTTP_CLIENT_CONFIG_TYPE)).isEqualTo("e"); + } + + @Test + public void explicitAsyncHttpClientProvided_httpClientConfigTypeIsExplicit() { + TestAsyncClient client = testAsyncClientBuilder() + .httpClient(mock(SdkAsyncHttpClient.class)) + .build(); + assertThat(client.clientConfiguration.option(HTTP_CLIENT_CONFIG_TYPE)).isEqualTo("e"); + } + + @Test + public void explicitAsyncHttpClientBuilderProvided_httpClientConfigTypeIsExplicit() { + TestAsyncClient client = testAsyncClientBuilder() + .httpClientBuilder((SdkAsyncHttpClient.Builder) serviceDefaults -> mock(SdkAsyncHttpClient.class)) + .build(); + assertThat(client.clientConfiguration.option(HTTP_CLIENT_CONFIG_TYPE)).isEqualTo("e"); + } + + @Test + public void syncHttpClientSetThenCleared_httpClientConfigTypeIsDefault() { + TestClient client = testClientBuilder() + .httpClient(mock(SdkHttpClient.class)) + .httpClient((SdkHttpClient) null) + .build(); + assertThat(client.clientConfiguration.option(HTTP_CLIENT_CONFIG_TYPE)).isEqualTo("d"); + } + + @Test + public void syncHttpClientBuilderSetThenCleared_httpClientConfigTypeIsDefault() { + TestClient client = testClientBuilder() + .httpClientBuilder((SdkHttpClient.Builder) serviceDefaults -> mock(SdkHttpClient.class)) + .httpClientBuilder((SdkHttpClient.Builder) null) + .build(); + assertThat(client.clientConfiguration.option(HTTP_CLIENT_CONFIG_TYPE)).isEqualTo("d"); + } + + @Test + public void asyncHttpClientSetThenCleared_httpClientConfigTypeIsDefault() { + TestAsyncClient client = testAsyncClientBuilder() + .httpClient(mock(SdkAsyncHttpClient.class)) + .httpClient((SdkAsyncHttpClient) null) + .build(); + assertThat(client.clientConfiguration.option(HTTP_CLIENT_CONFIG_TYPE)).isEqualTo("d"); + } + + @Test + public void asyncHttpClientBuilderSetThenCleared_httpClientConfigTypeIsDefault() { + TestAsyncClient client = testAsyncClientBuilder() + .httpClientBuilder((SdkAsyncHttpClient.Builder) serviceDefaults -> mock(SdkAsyncHttpClient.class)) + .httpClientBuilder((SdkAsyncHttpClient.Builder) null) + .build(); + assertThat(client.clientConfiguration.option(HTTP_CLIENT_CONFIG_TYPE)).isEqualTo("d"); + } + @Test public void clientBuilderFieldsHaveBeanEquivalents() throws Exception { // Mutating properties might not have bean equivalents. This is probably fine, since very few customers require diff --git a/docs/user-agent.md b/docs/user-agent.md index bb1dfde3a40..5a3b336c76c 100644 --- a/docs/user-agent.md +++ b/docs/user-agent.md @@ -19,3 +19,6 @@ The table below documents additional metadata the SDK may include in the `User-A ||`s`|"Stream". The response transformer adapts the response body to an `InputStream`.| ||`p`|"Publisher". The response transformer adapts the response body to an `SdkPublisher`.| ||`u`|"unknown"| +|`hc`||The HTTP client configuration type. Indicates whether the HTTP client was selected by default or explicitly configured by the user.| +||`d`|"Default". The HTTP client was auto-detected from the classpath.| +||`e`|"Explicit". The HTTP client was explicitly configured by the user via `httpClient()` or `httpClientBuilder()`.| diff --git a/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/useragent/HttpClientConfigTypeTrackingTest.java b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/useragent/HttpClientConfigTypeTrackingTest.java new file mode 100644 index 00000000000..3a7d6a052a5 --- /dev/null +++ b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/useragent/HttpClientConfigTypeTrackingTest.java @@ -0,0 +1,193 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.services.useragent; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.anyUrl; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.matching.RequestPatternBuilder.allRequests; +import static org.assertj.core.api.Assertions.assertThat; + +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.core.WireMockConfiguration; +import com.github.tomakehurst.wiremock.verification.LoggedRequest; +import java.net.URI; +import java.util.List; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.http.AbortableInputStream; +import software.amazon.awssdk.http.HttpExecuteResponse; +import software.amazon.awssdk.http.SdkHttpRequest; +import software.amazon.awssdk.http.SdkHttpResponse; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.protocolrestjsonwithconfig.ProtocolRestJsonWithConfigAsyncClient; +import software.amazon.awssdk.services.protocolrestjsonwithconfig.ProtocolRestJsonWithConfigClient; +import software.amazon.awssdk.testutils.service.http.MockAsyncHttpClient; +import software.amazon.awssdk.testutils.service.http.MockSyncHttpClient; +import software.amazon.awssdk.utils.StringInputStream; + +/** + * Tests that the HTTP client configuration type metadata (md/hc#d or md/hc#e) is correctly + * included in the User-Agent header based on whether the HTTP client was auto-detected or + * explicitly configured. + */ +public class HttpClientConfigTypeTrackingTest { + + private static final StaticCredentialsProvider CREDENTIALS = + StaticCredentialsProvider.create(AwsBasicCredentials.create("akid", "skid")); + + private WireMockServer wireMock; + private MockSyncHttpClient mockSyncHttpClient; + private MockAsyncHttpClient mockAsyncHttpClient; + + @BeforeEach + public void setup() { + wireMock = new WireMockServer(WireMockConfiguration.wireMockConfig().dynamicPort()); + wireMock.start(); + wireMock.stubFor(post(anyUrl()).willReturn(aResponse().withStatus(200).withBody("{}"))); + + mockSyncHttpClient = new MockSyncHttpClient(); + mockSyncHttpClient.stubNextResponse(mockResponse()); + + mockAsyncHttpClient = new MockAsyncHttpClient(); + mockAsyncHttpClient.stubNextResponse(mockResponse()); + } + + @AfterEach + public void teardown() { + wireMock.stop(); + } + + // --- Default HTTP client tests (no httpClient() call, auto-detected from classpath) --- + + @Test + public void syncClient_defaultHttpClient_containsHcDefault() { + ProtocolRestJsonWithConfigClient client = ProtocolRestJsonWithConfigClient + .builder() + .region(Region.US_WEST_2) + .credentialsProvider(CREDENTIALS) + .endpointOverride(URI.create("http://localhost:" + wireMock.port())) + .build(); + client.allTypes(r -> {}); + assertThat(lastWireMockUserAgent()).contains("md/hc#d"); + } + + @Test + public void asyncClient_defaultHttpClient_containsHcDefault() { + ProtocolRestJsonWithConfigAsyncClient client = ProtocolRestJsonWithConfigAsyncClient + .builder() + .region(Region.US_WEST_2) + .credentialsProvider(CREDENTIALS) + .endpointOverride(URI.create("http://localhost:" + wireMock.port())) + .build(); + client.allTypes(r -> {}).join(); + assertThat(lastWireMockUserAgent()).contains("md/hc#d"); + } + + // --- Explicit HTTP client tests (mock HTTP clients) --- + + @Test + public void syncClient_explicitHttpClient_containsHcExplicit() { + ProtocolRestJsonWithConfigClient client = ProtocolRestJsonWithConfigClient + .builder() + .region(Region.US_WEST_2) + .credentialsProvider(CREDENTIALS) + .httpClient(mockSyncHttpClient) + .build(); + client.allTypes(r -> {}); + assertThat(syncUserAgent()).contains("md/hc#e"); + } + + @Test + public void asyncClient_explicitHttpClient_containsHcExplicit() { + ProtocolRestJsonWithConfigAsyncClient client = ProtocolRestJsonWithConfigAsyncClient + .builder() + .region(Region.US_WEST_2) + .credentialsProvider(CREDENTIALS) + .httpClient(mockAsyncHttpClient) + .build(); + client.allTypes(r -> {}).join(); + assertThat(asyncUserAgent()).contains("md/hc#e"); + } + + // --- Persistence tests --- + + @Test + public void syncClient_persistsAcrossRequests() { + ProtocolRestJsonWithConfigClient client = ProtocolRestJsonWithConfigClient + .builder() + .region(Region.US_WEST_2) + .credentialsProvider(CREDENTIALS) + .httpClient(mockSyncHttpClient) + .build(); + client.allTypes(r -> {}); + String firstUserAgent = syncUserAgent(); + mockSyncHttpClient.stubNextResponse(mockResponse()); + client.allTypes(r -> {}); + String secondUserAgent = syncUserAgent(); + assertThat(firstUserAgent).contains("md/hc#e"); + assertThat(secondUserAgent).contains("md/hc#e"); + } + + @Test + public void asyncClient_persistsAcrossRequests() { + ProtocolRestJsonWithConfigAsyncClient client = ProtocolRestJsonWithConfigAsyncClient + .builder() + .region(Region.US_WEST_2) + .credentialsProvider(CREDENTIALS) + .httpClient(mockAsyncHttpClient) + .build(); + client.allTypes(r -> {}).join(); + String firstUserAgent = asyncUserAgent(); + mockAsyncHttpClient.stubNextResponse(mockResponse()); + client.allTypes(r -> {}).join(); + String secondUserAgent = asyncUserAgent(); + assertThat(firstUserAgent).contains("md/hc#e"); + assertThat(secondUserAgent).contains("md/hc#e"); + } + + // --- Helpers --- + + private String lastWireMockUserAgent() { + List requests = wireMock.findAll(allRequests()); + assertThat(requests).isNotEmpty(); + return requests.get(requests.size() - 1).getHeader("User-Agent"); + } + + private String syncUserAgent() { + SdkHttpRequest lastRequest = mockSyncHttpClient.getLastRequest(); + List headers = lastRequest.headers().get("User-Agent"); + assertThat(headers).isNotNull().hasSize(1); + return headers.get(0); + } + + private String asyncUserAgent() { + SdkHttpRequest lastRequest = mockAsyncHttpClient.getLastRequest(); + List headers = lastRequest.headers().get("User-Agent"); + assertThat(headers).isNotNull().hasSize(1); + return headers.get(0); + } + + private static HttpExecuteResponse mockResponse() { + return HttpExecuteResponse.builder() + .response(SdkHttpResponse.builder().statusCode(200).build()) + .responseBody(AbortableInputStream.create(new StringInputStream("{}"))) + .build(); + } +}