Skip to content

Commit 12b31e8

Browse files
authored
Handle 200 OK errors in PutObject (multipart) in S3 CRT client (#7084)
1 parent bb23595 commit 12b31e8

4 files changed

Lines changed: 425 additions & 0 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"type": "bugfix",
3+
"category": "Amazon S3",
4+
"contributor": "",
5+
"description": "Handle 200 OK errors in PutObject (multipart) in S3 CRT client"
6+
}

services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/crt/S3CrtResponseHandlerAdapter.java

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@
2121

2222
import java.nio.ByteBuffer;
2323
import java.time.Duration;
24+
import java.util.List;
25+
import java.util.Map;
26+
import java.util.Optional;
2427
import java.util.concurrent.CompletableFuture;
2528
import java.util.concurrent.ExecutionException;
2629
import java.util.concurrent.TimeUnit;
@@ -37,6 +40,8 @@
3740
import software.amazon.awssdk.crt.s3.S3FinishedResponseContext;
3841
import software.amazon.awssdk.crt.s3.S3MetaRequestProgress;
3942
import software.amazon.awssdk.crt.s3.S3MetaRequestResponseHandler;
43+
import software.amazon.awssdk.http.AbortableInputStream;
44+
import software.amazon.awssdk.http.SdkHttpFullResponse;
4045
import software.amazon.awssdk.http.SdkHttpResponse;
4146
import software.amazon.awssdk.http.async.SdkAsyncHttpResponseHandler;
4247
import software.amazon.awssdk.services.s3.model.S3Exception;
@@ -220,6 +225,15 @@ private void handleServiceError(int responseStatus, HttpHeader[] headers, byte[]
220225
s3Exception.addSuppressed(sdkClientException);
221226
failResponseHandlerAndFuture(s3Exception);
222227
notifyResponsePublisherErrorIfNeeded(s3Exception);
228+
} else if (responseStatus == 200) {
229+
// handleServiceError is only called when crtCode != SUCCESS. If the response status is 200, this is the
230+
// S3 "200 with error in body" case (e.g. CompleteMultipartUpload returning HTTP 200 with <Error> XML).
231+
// We wrap the response in ErrorFlaggedSdkHttpResponse which overrides isSuccessful() to return false.
232+
// This causes the downstream SDK pipeline (XmlResponseParserUtils, DecorateErrorFromResponseBodyUnmarshaller)
233+
// to parse the body and detect the error, producing a properly modeled S3Exception.
234+
SdkHttpFullResponse errorFlagged = new ErrorFlaggedSdkHttpResponse(errorResponse.build());
235+
initiateResponseHandling(errorFlagged);
236+
onErrorResponseComplete(errorPayload);
223237
} else {
224238
initiateResponseHandling(errorResponse.build());
225239
onErrorResponseComplete(errorPayload);
@@ -303,4 +317,148 @@ private static SdkHttpResponse.Builder populateSdkHttpResponse(SdkHttpResponse.B
303317

304318
private static class NoOpPublisherListener implements PublisherListener<S3MetaRequestProgress> {
305319
}
320+
321+
/**
322+
* An {@link SdkHttpFullResponse} wrapper that overrides {@link #isSuccessful()} to always return {@code false}.
323+
* This forces the SDK response pipeline ({@code XmlResponseParserUtils}, {@code DecorateErrorFromResponseBodyUnmarshaller})
324+
* to parse the response body even for HTTP 200 responses, enabling detection of S3's "200 with error in body" pattern.
325+
*
326+
* <p>The {@link #toBuilder()} method returns a builder whose {@code build()} also produces an
327+
* {@code ErrorFlaggedSdkHttpResponse}, ensuring the override survives the rebuild cycle in
328+
* {@code AsyncResponseHandler}.</p>
329+
*/
330+
static final class ErrorFlaggedSdkHttpResponse implements SdkHttpFullResponse {
331+
private final SdkHttpFullResponse delegate;
332+
333+
ErrorFlaggedSdkHttpResponse(SdkHttpResponse delegate) {
334+
// Ensure we have a full response to delegate to
335+
if (delegate instanceof SdkHttpFullResponse) {
336+
this.delegate = (SdkHttpFullResponse) delegate;
337+
} else {
338+
this.delegate = SdkHttpFullResponse.builder()
339+
.statusCode(delegate.statusCode())
340+
.headers(delegate.headers())
341+
.build();
342+
}
343+
}
344+
345+
private ErrorFlaggedSdkHttpResponse(SdkHttpFullResponse delegate, @SuppressWarnings("unused") boolean internal) {
346+
this.delegate = delegate;
347+
}
348+
349+
@Override
350+
public boolean isSuccessful() {
351+
return false;
352+
}
353+
354+
@Override
355+
public int statusCode() {
356+
return delegate.statusCode();
357+
}
358+
359+
@Override
360+
public Optional<String> statusText() {
361+
return delegate.statusText();
362+
}
363+
364+
@Override
365+
public Map<String, List<String>> headers() {
366+
return delegate.headers();
367+
}
368+
369+
@Override
370+
public Optional<AbortableInputStream> content() {
371+
return delegate.content();
372+
}
373+
374+
@Override
375+
public Builder toBuilder() {
376+
return new ErrorFlaggedBuilder(delegate.toBuilder());
377+
}
378+
}
379+
380+
/**
381+
* A builder that wraps a standard {@link SdkHttpFullResponse.Builder} but produces an
382+
* {@link ErrorFlaggedSdkHttpResponse} on {@code build()}, preserving the {@code isSuccessful()=false} override.
383+
*/
384+
private static final class ErrorFlaggedBuilder implements SdkHttpFullResponse.Builder {
385+
private final SdkHttpFullResponse.Builder delegate;
386+
387+
ErrorFlaggedBuilder(SdkHttpFullResponse.Builder delegate) {
388+
this.delegate = delegate;
389+
}
390+
391+
@Override
392+
public SdkHttpFullResponse build() {
393+
return new ErrorFlaggedSdkHttpResponse(delegate.build(), true);
394+
}
395+
396+
@Override
397+
public String statusText() {
398+
return delegate.statusText();
399+
}
400+
401+
@Override
402+
public SdkHttpFullResponse.Builder statusText(String statusText) {
403+
delegate.statusText(statusText);
404+
return this;
405+
}
406+
407+
@Override
408+
public int statusCode() {
409+
return delegate.statusCode();
410+
}
411+
412+
@Override
413+
public SdkHttpFullResponse.Builder statusCode(int statusCode) {
414+
delegate.statusCode(statusCode);
415+
return this;
416+
}
417+
418+
@Override
419+
public Map<String, List<String>> headers() {
420+
return delegate.headers();
421+
}
422+
423+
@Override
424+
public SdkHttpFullResponse.Builder putHeader(String headerName, List<String> headerValues) {
425+
delegate.putHeader(headerName, headerValues);
426+
return this;
427+
}
428+
429+
@Override
430+
public SdkHttpFullResponse.Builder appendHeader(String headerName, String headerValue) {
431+
delegate.appendHeader(headerName, headerValue);
432+
return this;
433+
}
434+
435+
@Override
436+
public SdkHttpFullResponse.Builder headers(Map<String, List<String>> headers) {
437+
delegate.headers(headers);
438+
return this;
439+
}
440+
441+
@Override
442+
public SdkHttpFullResponse.Builder removeHeader(String headerName) {
443+
delegate.removeHeader(headerName);
444+
return this;
445+
}
446+
447+
@Override
448+
public SdkHttpFullResponse.Builder clearHeaders() {
449+
delegate.clearHeaders();
450+
return this;
451+
}
452+
453+
@Override
454+
public AbortableInputStream content() {
455+
return delegate.content();
456+
}
457+
458+
@Override
459+
public SdkHttpFullResponse.Builder content(AbortableInputStream content) {
460+
delegate.content(content);
461+
return this;
462+
}
463+
}
306464
}

0 commit comments

Comments
 (0)