diff --git a/.changes/next-release/feature-AWSSDKforJavav2-6933447.json b/.changes/next-release/feature-AWSSDKforJavav2-6933447.json new file mode 100644 index 000000000000..05f531778c87 --- /dev/null +++ b/.changes/next-release/feature-AWSSDKforJavav2-6933447.json @@ -0,0 +1,6 @@ +{ + "type": "feature", + "category": "AWS SDK for Java v2", + "contributor": "", + "description": "Add the `@SdkAdvancedApi` annotation, which marks APIs that are error-prone to implement, override, call, or configure so that using them incorrectly compiles cleanly but can fail or misbehave at runtime. The annotation records structured guidance (the risky usage kind, an explanation of the contract to uphold, a safer alternative, and a documentation link) and is applied to several streaming and interceptor extension points, including AsyncRequestBody, AsyncResponseTransformer, ContentStreamProvider, the mutating ExecutionInterceptor content hooks, and the FUTURE_COMPLETION_EXECUTOR advanced client option." +} diff --git a/build-tools/pom.xml b/build-tools/pom.xml index b48807ad8caf..9d55cdf9379c 100644 --- a/build-tools/pom.xml +++ b/build-tools/pom.xml @@ -59,6 +59,12 @@ true + + + org.apache.maven.plugins + maven-surefire-plugin + 3.1.2 + @@ -74,6 +80,12 @@ spotbugs 4.8.6 + + org.junit.jupiter + junit-jupiter + 5.10.3 + test + diff --git a/build-tools/src/main/java/software/amazon/awssdk/buildtools/checkstyle/SdkAdvancedApiMemberLengthCheck.java b/build-tools/src/main/java/software/amazon/awssdk/buildtools/checkstyle/SdkAdvancedApiMemberLengthCheck.java new file mode 100644 index 000000000000..3db26f317689 --- /dev/null +++ b/build-tools/src/main/java/software/amazon/awssdk/buildtools/checkstyle/SdkAdvancedApiMemberLengthCheck.java @@ -0,0 +1,146 @@ +/* + * 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.buildtools.checkstyle; + +import com.puppycrawl.tools.checkstyle.api.AbstractCheck; +import com.puppycrawl.tools.checkstyle.api.DetailAST; +import com.puppycrawl.tools.checkstyle.api.FullIdent; +import com.puppycrawl.tools.checkstyle.api.TokenTypes; +import java.util.Arrays; +import java.util.List; + +/** + * Caps the length of the {@code guidance} and {@code saferAlternative} member values on any + * {@code @SdkAdvancedApi} usage. The values are written as multi-line string-literal concatenations, so this operates + * on the source AST and sums the decoded content of every concatenated string literal in the member's expression before + * comparing to {@code max}. + */ +public class SdkAdvancedApiMemberLengthCheck extends AbstractCheck { + + private static final String ANNOTATION_NAME = "SdkAdvancedApi"; + private static final List CHECKED_MEMBERS = Arrays.asList("guidance", "saferAlternative"); + + private int max = 1000; + + public void setMax(int max) { + this.max = max; + } + + @Override + public int[] getDefaultTokens() { + return getRequiredTokens(); + } + + @Override + public int[] getAcceptableTokens() { + return getRequiredTokens(); + } + + @Override + public int[] getRequiredTokens() { + return new int[] {TokenTypes.ANNOTATION}; + } + + @Override + public void visitToken(DetailAST annotation) { + if (!isSdkAdvancedApi(annotation)) { + return; + } + + for (DetailAST child = annotation.getFirstChild(); child != null; child = child.getNextSibling()) { + if (child.getType() != TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR) { + continue; + } + + DetailAST memberName = child.findFirstToken(TokenTypes.IDENT); + if (memberName == null || !CHECKED_MEMBERS.contains(memberName.getText())) { + continue; + } + + DetailAST expr = child.findFirstToken(TokenTypes.EXPR); + if (expr == null) { + continue; + } + + int length = concatenatedStringLength(expr); + if (length > max) { + log(memberName, String.format( + "@%s member '%s' is %d characters, which exceeds the maximum of %d. Tighten the wording.", + ANNOTATION_NAME, memberName.getText(), length, max)); + } + } + } + + private boolean isSdkAdvancedApi(DetailAST annotation) { + DetailAST nameNode = annotation.findFirstToken(TokenTypes.IDENT); + if (nameNode == null) { + nameNode = annotation.findFirstToken(TokenTypes.DOT); + } + if (nameNode == null) { + return false; + } + String name = FullIdent.createFullIdent(nameNode).getText(); + return name.equals(ANNOTATION_NAME) || name.endsWith("." + ANNOTATION_NAME); + } + + private int concatenatedStringLength(DetailAST node) { + int total = 0; + for (DetailAST child = node.getFirstChild(); child != null; child = child.getNextSibling()) { + if (child.getType() == TokenTypes.STRING_LITERAL) { + total += decodedLength(child.getText()); + } else { + total += concatenatedStringLength(child); + } + } + return total; + } + + /** + * Counts the logical characters in a string literal token (its text includes the surrounding quotes), treating each + * escape sequence as a single character so the count matches the text a consumer of the annotation would see. + */ + private int decodedLength(String literalWithQuotes) { + String body = literalWithQuotes; + if (body.length() >= 2 && body.charAt(0) == '"' && body.charAt(body.length() - 1) == '"') { + body = body.substring(1, body.length() - 1); + } + + int count = 0; + int i = 0; + while (i < body.length()) { + if (body.charAt(i) == '\\' && i + 1 < body.length()) { + char next = body.charAt(i + 1); + if (next == 'u') { + i += 6; + } else if (next >= '0' && next <= '7') { + int j = i + 1; + int digits = 0; + while (j < body.length() && digits < 3 && body.charAt(j) >= '0' && body.charAt(j) <= '7') { + j++; + digits++; + } + i = j; + } else { + i += 2; + } + } else { + i++; + } + count++; + } + return count; + } +} diff --git a/build-tools/src/main/resources/software/amazon/awssdk/checkstyle.xml b/build-tools/src/main/resources/software/amazon/awssdk/checkstyle.xml index 09c2082a8d40..857873374c96 100644 --- a/build-tools/src/main/resources/software/amazon/awssdk/checkstyle.xml +++ b/build-tools/src/main/resources/software/amazon/awssdk/checkstyle.xml @@ -444,6 +444,11 @@ + + + + + diff --git a/build-tools/src/test/java/software/amazon/awssdk/buildtools/checkstyle/SdkAdvancedApiMemberLengthCheckTest.java b/build-tools/src/test/java/software/amazon/awssdk/buildtools/checkstyle/SdkAdvancedApiMemberLengthCheckTest.java new file mode 100644 index 000000000000..450959528325 --- /dev/null +++ b/build-tools/src/test/java/software/amazon/awssdk/buildtools/checkstyle/SdkAdvancedApiMemberLengthCheckTest.java @@ -0,0 +1,165 @@ +/* + * 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.buildtools.checkstyle; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.puppycrawl.tools.checkstyle.Checker; +import com.puppycrawl.tools.checkstyle.DefaultConfiguration; +import com.puppycrawl.tools.checkstyle.api.AuditEvent; +import com.puppycrawl.tools.checkstyle.api.AuditListener; +import com.puppycrawl.tools.checkstyle.api.CheckstyleException; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class SdkAdvancedApiMemberLengthCheckTest { + + @TempDir + Path tempDir; + + @Test + void guidanceWithinLimit_passes() throws Exception { + String source = classWithMembers(repeat("a", 50), repeat("b", 50)); + assertEquals(Collections.emptyList(), runCheck(source, 1000)); + } + + @Test + void guidanceOverLimit_fails() throws Exception { + String source = classWithMembers(repeat("a", 1001), repeat("b", 50)); + List violations = runCheck(source, 1000); + assertEquals(1, violations.size()); + assertTrue(violations.get(0).contains("guidance"), violations.get(0)); + assertTrue(violations.get(0).contains("1001"), violations.get(0)); + } + + @Test + void saferAlternativeOverLimit_fails() throws Exception { + String source = classWithMembers(repeat("a", 50), repeat("b", 1001)); + List violations = runCheck(source, 1000); + assertEquals(1, violations.size()); + assertTrue(violations.get(0).contains("saferAlternative"), violations.get(0)); + } + + @Test + void multiLineConcatenation_sumsAllFragments() throws Exception { + String guidance = "\"" + repeat("a", 400) + "\"\n" + + " + \"" + repeat("b", 400) + "\"\n" + + " + \"" + repeat("c", 300) + "\""; + String source = classWithRawGuidance(guidance); + List violations = runCheck(source, 1000); + assertEquals(1, violations.size()); + assertTrue(violations.get(0).contains("1100"), violations.get(0)); + } + + @Test + void valueAtExactLimit_passes() throws Exception { + String source = classWithMembers(repeat("a", 1000), repeat("b", 1000)); + assertEquals(Collections.emptyList(), runCheck(source, 1000)); + } + + private static String repeat(String s, int n) { + return IntStream.range(0, n).mapToObj(i -> s).collect(Collectors.joining()); + } + + private static String classWithMembers(String guidance, String saferAlternative) { + return classWithRawGuidanceAndSafer("\"" + guidance + "\"", "\"" + saferAlternative + "\""); + } + + private static String classWithRawGuidance(String rawGuidanceExpr) { + return classWithRawGuidanceAndSafer(rawGuidanceExpr, "\"safe\""); + } + + private static String classWithRawGuidanceAndSafer(String rawGuidanceExpr, String rawSaferExpr) { + return "package p;\n" + + "@SdkAdvancedApi(\n" + + " cautionWhen = Usage.IMPLEMENTED,\n" + + " guidance = " + rawGuidanceExpr + ",\n" + + " saferAlternative = " + rawSaferExpr + ")\n" + + "public interface Foo {\n" + + "}\n"; + } + + private List runCheck(String source, int max) throws IOException, CheckstyleException { + File file = tempDir.resolve("Foo.java").toFile(); + Files.write(file.toPath(), source.getBytes(StandardCharsets.UTF_8)); + + DefaultConfiguration checkConfig = new DefaultConfiguration(SdkAdvancedApiMemberLengthCheck.class.getName()); + checkConfig.addAttribute("max", Integer.toString(max)); + + DefaultConfiguration treeWalker = new DefaultConfiguration("TreeWalker"); + treeWalker.addChild(checkConfig); + + DefaultConfiguration checker = new DefaultConfiguration("Checker"); + checker.addAttribute("charset", "UTF-8"); + checker.addChild(treeWalker); + + Checker c = new Checker(); + c.setModuleClassLoader(Thread.currentThread().getContextClassLoader()); + c.configure(checker); + + List messages = new ArrayList<>(); + c.addListener(new CollectingListener(messages)); + + c.process(Collections.singletonList(file)); + c.destroy(); + return messages; + } + + private static final class CollectingListener implements AuditListener { + private final List messages; + + private CollectingListener(List messages) { + this.messages = messages; + } + + @Override + public void addError(AuditEvent event) { + messages.add(event.getMessage()); + } + + @Override + public void addException(AuditEvent event, Throwable throwable) { + messages.add("EXCEPTION: " + throwable.getMessage()); + } + + @Override + public void auditStarted(AuditEvent event) { + } + + @Override + public void auditFinished(AuditEvent event) { + } + + @Override + public void fileStarted(AuditEvent event) { + } + + @Override + public void fileFinished(AuditEvent event) { + } + } +} diff --git a/core/annotations/src/main/java/software/amazon/awssdk/annotations/SdkAdvancedApi.java b/core/annotations/src/main/java/software/amazon/awssdk/annotations/SdkAdvancedApi.java new file mode 100644 index 000000000000..e7d5dab98bcb --- /dev/null +++ b/core/annotations/src/main/java/software/amazon/awssdk/annotations/SdkAdvancedApi.java @@ -0,0 +1,76 @@ +/* + * 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.annotations; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marks an API that is error-prone to use: implementing, overriding, or calling it incorrectly, or + * configuring it with an unsafe value, compiles cleanly but can fail or misbehave at runtime rather + * than reporting a clear error. + * + *

The annotation records the risk information on the API in a structured form: {@link #cautionWhen()} + * classifies which kind of use is error-prone, {@link #guidance()} explains the contract that must + * be upheld, {@link #saferAlternative()} points to a lower-risk approach, and {@link #link()} points + * to further documentation. + * + *

This is an advisory marker: it does not gate compilation and imposes no runtime behavior. + */ +@Documented +@Target({ElementType.TYPE, ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.FIELD}) +@Retention(RetentionPolicy.CLASS) +@SdkProtectedApi +public @interface SdkAdvancedApi { + + /** + * Which kind of use of this API is error-prone: IMPLEMENTED if the risk is in implementing + * or extending the annotated type, OVERRIDDEN if it is in overriding the annotated method, + * CONFIGURED if it is in setting the annotated field or option, and CALLED if it is in + * calling the annotated method (for example a factory that accepts an object you supply and does + * not shield you from that object's contract). Required. + */ + Usage cautionWhen(); + + /** + * Explains why this API is error-prone and what you must uphold to use it safely: the parts of + * the contract that are easy to get wrong and the failure that results if they are not met. + */ + String guidance(); + + /** + * An optional pointer to the recommended safer approach that satisfies the same need + * without the risk (for example, a factory method that implements the contract + * correctly). Empty when there is no direct alternative. + */ + String saferAlternative() default ""; + + /** + * An optional link to documentation explaining the risk and correct usage in more + * depth. Empty when there is no dedicated page. + */ + String link() default ""; + + enum Usage { + IMPLEMENTED, + OVERRIDDEN, + CONFIGURED, + CALLED + } +} diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/async/AsyncRequestBody.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/async/AsyncRequestBody.java index 36a2d7eb647c..d762dc15a809 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/async/AsyncRequestBody.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/async/AsyncRequestBody.java @@ -29,6 +29,8 @@ import java.util.function.Consumer; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; +import software.amazon.awssdk.annotations.SdkAdvancedApi; +import software.amazon.awssdk.annotations.SdkAdvancedApi.Usage; import software.amazon.awssdk.annotations.SdkProtectedApi; import software.amazon.awssdk.annotations.SdkPublicApi; import software.amazon.awssdk.core.FileRequestBodyConfiguration; @@ -63,6 +65,15 @@ * @see ByteBuffersAsyncRequestBody */ @SdkPublicApi +@SdkAdvancedApi( + cautionWhen = Usage.IMPLEMENTED, + guidance = "This is a reactive-streams Publisher and must obey the reactive-streams specification. " + + "The SDK re-subscribes on each retry attempt, " + + "so to support retries, reproduce the full content on every subscribe; a body that cannot should fail the " + + "subscriber on a later subscribe rather than emit partial content.", + saferAlternative = "Prefer the AsyncRequestBody.fromFile/fromBytes/fromInputStream factories. If you must supply a " + + "custom Publisher, build it with an established reactive-streams library such as RxJava or Reactor rather " + + "than implementing Publisher by hand.") public interface AsyncRequestBody extends SdkPublisher { /** @@ -94,6 +105,16 @@ default String body() { * @param publisher Publisher of source data * @return Implementation of {@link AsyncRequestBody} that produces data send by the publisher */ + @SdkAdvancedApi( + cautionWhen = Usage.CALLED, + guidance = "The returned AsyncRequestBody passes each subscribe straight through to the supplied publisher, so " + + "the publisher must itself meet the AsyncRequestBody contract: obey the reactive-streams " + + "specification, and since the SDK re-subscribes on each " + + "retry attempt, reproduce the full content on every subscribe to support retries; a publisher that " + + "cannot should fail the subscriber on a later subscribe rather than emit partial content.", + saferAlternative = "Prefer the AsyncRequestBody.fromFile/fromBytes/fromInputStream factories, which do not " + + "require the caller to supply a contract-compliant publisher. If you must supply one, build it with an " + + "established reactive-streams library such as RxJava or Reactor rather than by hand.") static AsyncRequestBody fromPublisher(Publisher publisher) { return new AsyncRequestBody() { diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/async/AsyncResponseTransformer.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/async/AsyncResponseTransformer.java index dd4a5140170f..70ff1e6aef50 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/async/AsyncResponseTransformer.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/async/AsyncResponseTransformer.java @@ -23,6 +23,8 @@ import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; +import software.amazon.awssdk.annotations.SdkAdvancedApi; +import software.amazon.awssdk.annotations.SdkAdvancedApi.Usage; import software.amazon.awssdk.annotations.SdkProtectedApi; import software.amazon.awssdk.annotations.SdkPublicApi; import software.amazon.awssdk.core.FileTransformerConfiguration; @@ -86,6 +88,15 @@ * @param Type this response handler produces. I.E. the type you are transforming the response into. */ @SdkPublicApi +@SdkAdvancedApi( + cautionWhen = Usage.IMPLEMENTED, + guidance = "prepare() is called on each request attempt; if the CompletableFuture it returned on a previous " + + "attempt has already completed, it must return a new instance so the result of a retry is not lost, and " + + "exceptionOccurred() should release any resources the attempt opened (for example an open file channel) " + + "so retries do not leak them. onStream() receives the response body as a reactive-streams Publisher that " + + "the implementation subscribes to, and that subscriber must comply with the reactive-streams " + + "specification; a subscriber that never requests data stalls the response.", + saferAlternative = "Prefer the AsyncResponseTransformer.toFile/toBytes/toBlockingInputStream factories.") public interface AsyncResponseTransformer { /** * Initial call to enable any setup required before the response is handled. @@ -304,6 +315,14 @@ static AsyncResponseTransformer> * @param Pojo response type. * @return AsyncResponseTransformer instance. */ + @SdkAdvancedApi( + cautionWhen = Usage.CALLED, + guidance = "The returned ResponsePublisher is a reactive-streams Publisher you must subscribe to and drive: " + + "your subscriber must obey the reactive-streams specification and honor back-pressure. A subscriber " + + "that never requests data stalls the response, and requesting unbounded data can exhaust memory.", + saferAlternative = "Prefer the fully-managed AsyncResponseTransformer.toFile/toBytes factories, which do not " + + "hand you a publisher to drive. If you do consume the publisher, use an established reactive-streams " + + "library such as RxJava or Reactor rather than a hand-written Subscriber.") static AsyncResponseTransformer> toPublisher() { return new PublisherAsyncResponseTransformer<>(); } @@ -331,6 +350,14 @@ static AsyncResponseTransformer AsyncResponseTransformer> toPublisher(Duration timeout) { return new PublisherAsyncResponseTransformer<>(timeout); diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/SdkAdvancedAsyncClientOption.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/SdkAdvancedAsyncClientOption.java index e37ba953211d..973fc0c97f1a 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/SdkAdvancedAsyncClientOption.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/SdkAdvancedAsyncClientOption.java @@ -18,6 +18,8 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; import java.util.concurrent.ThreadPoolExecutor; +import software.amazon.awssdk.annotations.SdkAdvancedApi; +import software.amazon.awssdk.annotations.SdkAdvancedApi.Usage; import software.amazon.awssdk.annotations.SdkPublicApi; /** @@ -52,6 +54,13 @@ public final class SdkAdvancedAsyncClientOption extends ClientOption { * within the I/O thread because it may block the I/O thread and cause deadlock, especially if you are sending * another SDK request in the {@link CompletableFuture} chain since the SDK may perform blocking calls in some cases. */ + @SdkAdvancedApi( + cautionWhen = Usage.CONFIGURED, + guidance = "An executor that does not dispatch to a separate thread (e.g. Runnable::run) runs the " + + "completion inline on the calling thread and can deadlock the client, especially when the " + + "future chain issues another SDK request. If you set a custom executor, verify it under your " + + "own load tests, including request chains that issue further SDK calls.", + saferAlternative = "Leave unset to use the SDK-managed per-client ThreadPoolExecutor.") public static final SdkAdvancedAsyncClientOption FUTURE_COMPLETION_EXECUTOR = new SdkAdvancedAsyncClientOption<>(Executor.class); diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/interceptor/ExecutionInterceptor.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/interceptor/ExecutionInterceptor.java index 7ade08249da2..73326f170095 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/interceptor/ExecutionInterceptor.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/interceptor/ExecutionInterceptor.java @@ -19,6 +19,8 @@ import java.nio.ByteBuffer; import java.util.Optional; import org.reactivestreams.Publisher; +import software.amazon.awssdk.annotations.SdkAdvancedApi; +import software.amazon.awssdk.annotations.SdkAdvancedApi.Usage; import software.amazon.awssdk.annotations.SdkPublicApi; import software.amazon.awssdk.core.SdkRequest; import software.amazon.awssdk.core.SdkResponse; @@ -183,11 +185,32 @@ default SdkHttpRequest modifyHttpRequest(Context.ModifyHttpRequest context, return context.httpRequest(); } + @SdkAdvancedApi( + cautionWhen = Usage.OVERRIDDEN, + guidance = "Replacing the request body from an interceptor is discouraged: the returned RequestBody becomes the " + + "content the SDK sends, and the request's Content-Length and content hash or checksum are derived from " + + "it, so a body whose content or length does not match what the operation expects corrupts the request " + + "or is rejected by the service. Return the body reported by context.requestBody() unless you have a " + + "specific reason to replace it.", + saferAlternative = "Return context.requestBody() unchanged. If you must supply a different body, build it with " + + "the RequestBody factories rather than a hand-written ContentStreamProvider.") default Optional modifyHttpContent(Context.ModifyHttpRequest context, ExecutionAttributes executionAttributes) { return context.requestBody(); } + @SdkAdvancedApi( + cautionWhen = Usage.OVERRIDDEN, + guidance = "Replacing the async request body from an interceptor is discouraged: the returned AsyncRequestBody " + + "becomes the content the SDK sends, and the request's Content-Length and content hash or checksum are " + + "derived from it, so a body whose content or length does not match what the operation expects corrupts " + + "the request or is rejected by the service. It must also comply with the reactive-streams " + + "specification; the SDK re-subscribes to it " + + "on each retry attempt, so to support retries it must reproduce the full content on every subscribe. " + + "Return the body reported by context.asyncRequestBody() unless you have a specific reason to replace " + + "it.", + saferAlternative = "Return context.asyncRequestBody() unchanged. If you must supply a different body, build it " + + "with the AsyncRequestBody factories or an established reactive-streams library.") default Optional modifyAsyncHttpContent(Context.ModifyHttpRequest context, ExecutionAttributes executionAttributes) { return context.asyncRequestBody(); diff --git a/http-client-spi/src/main/java/software/amazon/awssdk/http/ContentStreamProvider.java b/http-client-spi/src/main/java/software/amazon/awssdk/http/ContentStreamProvider.java index 4ae96838d8b4..714a960a9262 100644 --- a/http-client-spi/src/main/java/software/amazon/awssdk/http/ContentStreamProvider.java +++ b/http-client-spi/src/main/java/software/amazon/awssdk/http/ContentStreamProvider.java @@ -24,6 +24,8 @@ import java.util.Arrays; import java.util.Map; import java.util.function.Supplier; +import software.amazon.awssdk.annotations.SdkAdvancedApi; +import software.amazon.awssdk.annotations.SdkAdvancedApi.Usage; import software.amazon.awssdk.annotations.SdkProtectedApi; import software.amazon.awssdk.annotations.SdkPublicApi; import software.amazon.awssdk.utils.IoUtils; @@ -41,6 +43,13 @@ */ @SdkPublicApi @FunctionalInterface +@SdkAdvancedApi( + cautionWhen = Usage.IMPLEMENTED, + guidance = "newStream() must return a stream positioned at the start of the content on every call, closing any " + + "previous stream it opened. A stale or unreset stream corrupts the request body and breaks retries, " + + "which re-read the content.", + saferAlternative = "Prefer the ContentStreamProvider.fromInputStream/fromByteArray/fromString factories, which " + + "handle reset correctly.") public interface ContentStreamProvider { /** * Create {@link ContentStreamProvider} from a byte array. This will copy the contents of the byte array.