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+
116package software .amazon .smithy .aws .go .codegen .customization .service .s3 ;
217
18+ import java .util .HashSet ;
319import java .util .List ;
20+ import java .util .Optional ;
421import java .util .Set ;
522
623import software .amazon .smithy .aws .go .codegen .customization .AwsCustomGoDependency ;
1128import software .amazon .smithy .model .Model ;
1229import software .amazon .smithy .model .shapes .OperationShape ;
1330import software .amazon .smithy .model .shapes .ServiceShape ;
14- import software .amazon .smithy .utils .ListUtils ;
15- import software .amazon .smithy .utils .SetUtils ;
31+ import software .amazon .smithy .model .shapes .ShapeId ;
32+ import software .amazon .smithy .model .shapes .StructureShape ;
33+ import software .amazon .smithy .model .traits .HttpPayloadTrait ;
34+ import software .amazon .smithy .model .traits .StreamingTrait ;
1635
1736/**
1837 * Adds middleware to handle S3 response errors with 200 ok status code.
38+ *
39+ * Per internal specification, this customization MUST be applied to all S3
40+ * operations with a structured XML response. The response MUST NOT have a
41+ * streaming binary payload or event-stream. In Smithy terms, this means all
42+ * S3 operations whose output does not contain a member with @httpPayload
43+ * targeting a @streaming blob, @streaming union, or string.
44+ *
45+ * Coverage is being rolled out in waves gated by call volume. Once all waves
46+ * have soaked, remove the wave sets and the isInEnabledWave check — the
47+ * model-driven logic in supports200Error is the correct final behavior.
1948 */
2049public class S3ErrorWith200Status implements GoIntegration {
21- private static String ADD_ERROR_HANDLER_INTERNAL = "HandleResponseErrorWith200Status" ;
50+ private static final String ADD_ERROR_HANDLER_INTERNAL = "HandleResponseErrorWith200Status" ;
51+
52+ // Already covered on main — always enabled.
53+ private static final Set <String > ORIGINAL_OPERATIONS = Set .of (
54+ "CopyObject" , "UploadPartCopy" , "CompleteMultipartUpload"
55+ );
56+
57+ // Wave 1: < 1B req/week. Very low risk.
58+ private static final Set <String > WAVE_1_OPERATIONS = Set .of (
59+ "AbortMultipartUpload" ,
60+ "CreateBucketMetadataConfiguration" ,
61+ "CreateBucketMetadataTableConfiguration" ,
62+ "CreateSession" ,
63+ "DeleteBucket" ,
64+ "DeleteBucketAnalyticsConfiguration" ,
65+ "DeleteBucketCors" ,
66+ "DeleteBucketEncryption" ,
67+ "DeleteBucketIntelligentTieringConfiguration" ,
68+ "DeleteBucketInventoryConfiguration" ,
69+ "DeleteBucketLifecycle" ,
70+ "DeleteBucketMetadataConfiguration" ,
71+ "DeleteBucketMetadataTableConfiguration" ,
72+ "DeleteBucketMetricsConfiguration" ,
73+ "DeleteBucketOwnershipControls" ,
74+ "DeleteBucketPolicy" ,
75+ "DeleteBucketReplication" ,
76+ "DeleteBucketTagging" ,
77+ "DeleteBucketWebsite" ,
78+ "DeleteObjectAnnotation" ,
79+ "DeleteObjectTagging" ,
80+ "DeletePublicAccessBlock" ,
81+ "GetBucketAbac" ,
82+ "GetBucketAnalyticsConfiguration" ,
83+ "GetBucketIntelligentTieringConfiguration" ,
84+ "GetBucketInventoryConfiguration" ,
85+ "GetBucketMetadataConfiguration" ,
86+ "GetBucketMetadataTableConfiguration" ,
87+ "GetBucketMetricsConfiguration" ,
88+ "GetBucketTagging" ,
89+ "GetObjectAcl" ,
90+ "GetObjectAttributes" ,
91+ "GetObjectLegalHold" ,
92+ "GetObjectRetention" ,
93+ "GetPublicAccessBlock" ,
94+ "ListBucketAnalyticsConfigurations" ,
95+ "ListBucketIntelligentTieringConfigurations" ,
96+ "ListBucketInventoryConfigurations" ,
97+ "ListBucketMetricsConfigurations" ,
98+ "ListMultipartUploads" ,
99+ "ListObjectAnnotations" ,
100+ "ListParts" ,
101+ "PutBucketAbac" ,
102+ "PutBucketAccelerateConfiguration" ,
103+ "PutBucketCors" ,
104+ "PutBucketIntelligentTieringConfiguration" ,
105+ "PutBucketInventoryConfiguration" ,
106+ "PutBucketLifecycleConfiguration" ,
107+ "PutBucketLogging" ,
108+ "PutBucketMetricsConfiguration" ,
109+ "PutBucketNotificationConfiguration" ,
110+ "PutBucketOwnershipControls" ,
111+ "PutBucketReplication" ,
112+ "PutBucketRequestPayment" ,
113+ "PutBucketTagging" ,
114+ "PutBucketVersioning" ,
115+ "PutBucketWebsite" ,
116+ "PutObjectAnnotation" ,
117+ "PutObjectLegalHold" ,
118+ "PutObjectLockConfiguration" ,
119+ "PutPublicAccessBlock" ,
120+ "RenameObject" ,
121+ "RestoreObject" ,
122+ "UpdateBucketMetadataAnnotationTableConfiguration" ,
123+ "UpdateBucketMetadataInventoryTableConfiguration" ,
124+ "UpdateBucketMetadataJournalTableConfiguration" ,
125+ "UpdateObjectEncryption" ,
126+ "WriteGetObjectResponse"
127+ );
128+
129+ // Wave 2: 600M–5B req/week. Low-medium risk.
130+ private static final Set <String > WAVE_2_OPERATIONS = Set .of (
131+ "GetBucketAccelerateConfiguration" ,
132+ "GetBucketCors" ,
133+ "GetBucketLifecycleConfiguration" ,
134+ "GetBucketLogging" ,
135+ "GetBucketNotificationConfiguration" ,
136+ "GetBucketOwnershipControls" ,
137+ "GetBucketPolicyStatus" ,
138+ "GetBucketReplication" ,
139+ "GetBucketRequestPayment" ,
140+ "GetBucketVersioning" ,
141+ "GetBucketWebsite" ,
142+ "GetObjectLockConfiguration" ,
143+ "GetPublicAccessBlock" ,
144+ "ListBuckets" ,
145+ "ListDirectoryBuckets" ,
146+ "PutBucketAcl" ,
147+ "PutBucketEncryption" ,
148+ "PutBucketPolicy"
149+ );
150+
151+ // Wave 3: 5B–20B req/week. Medium risk.
152+ private static final Set <String > WAVE_3_OPERATIONS = Set .of (
153+ "CreateMultipartUpload" ,
154+ "DeleteObjects" ,
155+ "GetBucketAcl" ,
156+ "GetBucketEncryption" ,
157+ "GetBucketLocation" ,
158+ "GetObjectTagging" ,
159+ "HeadBucket" ,
160+ "ListObjectVersions" ,
161+ "PutObjectRetention" ,
162+ "PutObjectTagging"
163+ );
22164
23- // list of operations for which this customization is valid.
24- private static Set <String > customizedOperations = SetUtils .of (
25- "CopyObject" , "UploadPartCopy" , "CompleteMultipartUpload" );
165+ // Wave 4: 50B+ req/week. Medium-high risk (volume).
166+ private static final Set <String > WAVE_4_OPERATIONS = Set .of (
167+ "CreateBucket" ,
168+ "DeleteObject" ,
169+ "HeadObject" ,
170+ "ListObjects" ,
171+ "ListObjectsV2" ,
172+ "PutObject" ,
173+ "UploadPart"
174+ );
175+
176+ /**
177+ * Combined set of all currently enabled operations. To enable a wave,
178+ * add it to this set. To finish rollout, delete all wave sets and the
179+ * isInEnabledWave check entirely.
180+ */
181+ private static final Set <String > ENABLED_OPERATIONS = buildEnabledOperations ();
182+
183+ private static Set <String > buildEnabledOperations () {
184+ Set <String > enabled = new HashSet <>(ORIGINAL_OPERATIONS );
185+ enabled .addAll (WAVE_1_OPERATIONS );
186+ // enabled.addAll(WAVE_2_OPERATIONS);
187+ // enabled.addAll(WAVE_3_OPERATIONS);
188+ // enabled.addAll(WAVE_4_OPERATIONS);
189+ return Set .copyOf (enabled );
190+ }
26191
27192 @ Override
28193 public byte getOrder () {
@@ -33,7 +198,7 @@ public byte getOrder() {
33198
34199 @ Override
35200 public List <RuntimeClientPlugin > getClientPlugins () {
36- return ListUtils .of (
201+ return List .of (
37202 RuntimeClientPlugin .builder ()
38203 .operationPredicate (S3ErrorWith200Status ::supports200Error )
39204 .registerMiddleware (MiddlewareRegistrar .builder ()
@@ -44,13 +209,36 @@ public List<RuntimeClientPlugin> getClientPlugins() {
44209 );
45210 }
46211
47- // returns true if the operation supports error response with 200 ok status code
48- private static boolean supports200Error (Model model , ServiceShape service , OperationShape operation ){
212+ /**
213+ * Returns true if the operation supports error response with 200 ok status code.
214+ *
215+ * Uses a two-layer check:
216+ * 1. Model-driven: excludes operations with @httpPayload targeting @streaming or string.
217+ * 2. Wave gate: only enables operations that are in a currently-active wave.
218+ *
219+ * Once all waves are shipped and baked, remove the wave gate (step 2) and
220+ * the model-driven logic alone becomes the final behavior.
221+ */
222+ private static boolean supports200Error (Model model , ServiceShape service , OperationShape operation ) {
49223 if (!isS3Service (model , service )) {
50224 return false ;
51225 }
52226
53- return customizedOperations .contains (operation .getId ().getName (service ));
227+ // Model-driven exclusion: this is the correct final logic.
228+ Optional <ShapeId > output = operation .getOutput ();
229+ if (output .isPresent ()) {
230+ StructureShape outputShape = model .expectShape (output .get (), StructureShape .class );
231+ boolean excluded = outputShape .getAllMembers ().values ().stream ()
232+ .filter (member -> member .hasTrait (HttpPayloadTrait .class ))
233+ .map (member -> model .expectShape (member .getTarget ()))
234+ .anyMatch (target -> target .hasTrait (StreamingTrait .class ) || target .isStringShape ());
235+ if (excluded ) {
236+ return false ;
237+ }
238+ }
239+
240+ // Wave gate: remove this check once all waves are shipped and baked.
241+ return ENABLED_OPERATIONS .contains (operation .getId ().getName (service ));
54242 }
55243
56244 // returns true if service is s3
0 commit comments