|
21 | 21 |
|
22 | 22 | import java.nio.ByteBuffer; |
23 | 23 | import java.time.Duration; |
| 24 | +import java.util.List; |
| 25 | +import java.util.Map; |
| 26 | +import java.util.Optional; |
24 | 27 | import java.util.concurrent.CompletableFuture; |
25 | 28 | import java.util.concurrent.ExecutionException; |
26 | 29 | import java.util.concurrent.TimeUnit; |
|
37 | 40 | import software.amazon.awssdk.crt.s3.S3FinishedResponseContext; |
38 | 41 | import software.amazon.awssdk.crt.s3.S3MetaRequestProgress; |
39 | 42 | import software.amazon.awssdk.crt.s3.S3MetaRequestResponseHandler; |
| 43 | +import software.amazon.awssdk.http.AbortableInputStream; |
| 44 | +import software.amazon.awssdk.http.SdkHttpFullResponse; |
40 | 45 | import software.amazon.awssdk.http.SdkHttpResponse; |
41 | 46 | import software.amazon.awssdk.http.async.SdkAsyncHttpResponseHandler; |
42 | 47 | import software.amazon.awssdk.services.s3.model.S3Exception; |
@@ -220,6 +225,15 @@ private void handleServiceError(int responseStatus, HttpHeader[] headers, byte[] |
220 | 225 | s3Exception.addSuppressed(sdkClientException); |
221 | 226 | failResponseHandlerAndFuture(s3Exception); |
222 | 227 | 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); |
223 | 237 | } else { |
224 | 238 | initiateResponseHandling(errorResponse.build()); |
225 | 239 | onErrorResponseComplete(errorPayload); |
@@ -303,4 +317,148 @@ private static SdkHttpResponse.Builder populateSdkHttpResponse(SdkHttpResponse.B |
303 | 317 |
|
304 | 318 | private static class NoOpPublisherListener implements PublisherListener<S3MetaRequestProgress> { |
305 | 319 | } |
| 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 | + } |
306 | 464 | } |
0 commit comments