1515
1616package software .amazon .smithy .aws .go .codegen .customization .service .s3 ;
1717
18+ import java .util .HashSet ;
1819import java .util .List ;
1920import java .util .Optional ;
21+ import java .util .Set ;
2022
2123import software .amazon .smithy .aws .go .codegen .customization .AwsCustomGoDependency ;
2224import software .amazon .smithy .go .codegen .SymbolUtils ;
2325import software .amazon .smithy .go .codegen .integration .GoIntegration ;
2426import software .amazon .smithy .go .codegen .integration .MiddlewareRegistrar ;
2527import software .amazon .smithy .go .codegen .integration .RuntimeClientPlugin ;
2628import software .amazon .smithy .model .Model ;
27- import software .amazon .smithy .model .shapes .MemberShape ;
2829import software .amazon .smithy .model .shapes .OperationShape ;
2930import software .amazon .smithy .model .shapes .ServiceShape ;
30- import software .amazon .smithy .model .shapes .Shape ;
3131import software .amazon .smithy .model .shapes .ShapeId ;
3232import software .amazon .smithy .model .shapes .StructureShape ;
3333import software .amazon .smithy .model .traits .HttpPayloadTrait ;
4141 * streaming binary payload or event-stream. In Smithy terms, this means all
4242 * S3 operations whose output does not contain a member with @httpPayload
4343 * 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.
4448 */
4549public class S3ErrorWith200Status implements GoIntegration {
46- 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+ );
164+
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+ }
47191
48192 @ Override
49193 public byte getOrder () {
@@ -68,29 +212,33 @@ public List<RuntimeClientPlugin> getClientPlugins() {
68212 /**
69213 * Returns true if the operation supports error response with 200 ok status code.
70214 *
71- * Per internal specification, this applies to all S3 operations whose output
72- * does NOT contain an @httpPayload member targeting either:
73- * - a shape with the @streaming trait (blob or event stream)
74- * - a string shape
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.
75221 */
76222 private static boolean supports200Error (Model model , ServiceShape service , OperationShape operation ) {
77223 if (!isS3Service (model , service )) {
78224 return false ;
79225 }
80226
227+ // Model-driven exclusion: this is the correct final logic.
81228 Optional <ShapeId > output = operation .getOutput ();
82- if (!output .isPresent ()) {
83- // No output structure means a structured (empty) XML response, apply the check.
84- return true ;
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+ }
85238 }
86239
87- StructureShape outputShape = model .expectShape (output .get (), StructureShape .class );
88-
89- // Exclude if any @httpPayload member targets a @streaming shape or a string.
90- return outputShape .getAllMembers ().values ().stream ()
91- .filter (member -> member .hasTrait (HttpPayloadTrait .class ))
92- .map (member -> model .expectShape (member .getTarget ()))
93- .noneMatch (target -> target .hasTrait (StreamingTrait .class ) || target .isStringShape ());
240+ // Wave gate: remove this check once all waves are shipped and baked.
241+ return ENABLED_OPERATIONS .contains (operation .getId ().getName (service ));
94242 }
95243
96244 // returns true if service is s3
0 commit comments