Skip to content

[bug] S3:HeadBucket returns an XML response body on 4xx; real S3 omits it #3

@mizzy

Description

@mizzy

Affected AWS service

s3

Summary

winterbaume-s3's HeadBucket handler returns an XML <Error> body on a 4xx response. The AWS S3 documentation for HeadBucket states that 4xx responses do not include a message body, and the presence of that body is what makes aws-sdk-rust leave the error as Unhandled instead of resolving it into the typed HeadBucketError::NotFound variant.

Reproduction

Cargo.toml:

[dependencies]
aws-config       = "1"
aws-sdk-s3       = "1"
tokio            = { version = "1", features = ["full"] }
winterbaume-core = "0.2"
winterbaume-s3   = "0.2"

src/main.rs:

use aws_sdk_s3::config::BehaviorVersion;
use winterbaume_core::MockAws;
use winterbaume_s3::S3Service;

#[tokio::main]
async fn main() {
    let mock = MockAws::builder().with_service(S3Service::new()).build();
    let config = aws_config::defaults(BehaviorVersion::latest())
        .http_client(mock.http_client())
        .credentials_provider(mock.credentials_provider())
        .region(aws_sdk_s3::config::Region::new("us-east-1"))
        .load().await;
    let client = aws_sdk_s3::Client::new(&config);

    let err = client.head_bucket().bucket("does-not-exist").send().await
        .expect_err("head_bucket on missing bucket must fail");
    if let aws_sdk_s3::error::SdkError::ServiceError(svc) = &err {
        let raw = svc.raw();
        println!("HTTP status: {}", raw.status().as_u16());
        for (k, v) in raw.headers().iter() { println!("header: {k}: {v}"); }
        println!("inner: {:?}", svc.err());
    }
    let typed_not_found = matches!(
        err.as_service_error(),
        Some(aws_sdk_s3::operation::head_bucket::HeadBucketError::NotFound(_))
    );
    println!("typed HeadBucketError::NotFound match: {typed_not_found}");
}

Expected behaviour

Per the AWS S3 HeadBucket API documentation:

If the bucket doesn't exist or you don't have permission to access it, the HEAD request returns a generic 400 Bad Request, 403 Forbidden, or 404 Not Found HTTP status code. A message body isn't included, so you can't determine the exception beyond these HTTP response codes.

The 4xx response should carry no body; the bucket-absence signal is the HTTP status alone. With a body-less response, aws-sdk-rust resolves the error into the typed HeadBucketError::NotFound variant.

Actual behaviour

The repro above prints:

HTTP status: 404
header: content-type: text/xml
inner: Unhandled(Unhandled { source: ErrorMetadata { code: Some("NoSuchBucket"), message: Some("The specified bucket does not exist."), extras: None }, ... })
typed HeadBucketError::NotFound match: false

The response carries a text/xml body (an <Error><Code>NoSuchBucket</Code>...</Error> document), and aws-sdk-rust leaves the error as Unhandled rather than the typed HeadBucketError::NotFound variant.

The body is what causes the Unhandled classification. I verified this locally by patching handle_head_bucket to overwrite the error response's body with Bytes::new() (leaving the 404 status and content-type: text/xml header unchanged) and re-running the same repro. The output then becomes:

HTTP status: 404
header: content-type: text/xml
inner: NotFound(NotFound { message: None, meta: ErrorMetadata { code: Some("NotFound"), message: None, extras: None } })
typed HeadBucketError::NotFound match: true

So dropping the body alone is sufficient to restore the typed variant resolution — no other response field needs to change.

winterbaume version / commit

4b4cc9a

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions