Repository: floci-io/floci (2026-07-11T10:02:14Z)

Issue:
[BUG] S3 DeleteBucketReplication deletes the entire bucket
## Service

S3

## AWS API Action

DeleteBucketReplication

## Expected behavior

Real S3 `DELETE /{bucket}?replication` removes only the replication configuration and returns `204`. A sub-resource-qualified DELETE never deletes the bucket. `HeadBucket` afterward returns `200`.

## Actual behavior

`delete-bucket-replication` returns success, but the bucket itself is destroyed. The `?replication` sub-resource is not handled and falls through to the unqualified `DeleteBucket`. A subsequent `head-bucket` returns `404`:

```
An error occurred (404) when calling the HeadBucket operation: Not Found
```

## Reproduction

```bash
aws --endpoint-url http://localhost:4566 s3api create-bucket --bucket repro-bucket
aws --endpoint-url http://localhost:4566 s3api delete-bucket-replication --bucket repro-bucket
aws --endpoint-url http://localhost:4566 s3api head-bucket --bucket repro-bucket
# => An error occurred (404) when calling the HeadBucket operation: Not Found
```

## Environment

- Floci version / image tag: 1.5.32
- Java SDK version (if applicable): n/a — AWS CLI 2.35.7
- How you're running Floci: Docker



Discussion:

Patch:
diff --git a/src/main/java/io/github/hectorvent/floci/services/s3/S3Controller.java b/src/main/java/io/github/hectorvent/floci/services/s3/S3Controller.java
index 891cf2c8bc..3b1a1c75ce 100644
--- a/src/main/java/io/github/hectorvent/floci/services/s3/S3Controller.java
+++ b/src/main/java/io/github/hectorvent/floci/services/s3/S3Controller.java
@@ -314,6 +314,12 @@ public Response deleteBucket(@PathParam("bucket") String bucket,
                 s3Service.deleteBucketOwnershipControls(bucket);
                 return Response.noContent().build();
             }
+            if (hasQueryParam(uriInfo, "replication")) {
+                // Floci does not model bucket replication; DeleteBucketReplication is a
+                // no-op that always returns 204, matching real S3. Crucially it must be
+                // handled here so it does NOT fall through to deleting the whole bucket.
+                return Response.noContent().build();
+            }
             s3Service.deleteBucket(bucket);
             return Response.noContent().build();
         } catch (AwsException e) {


Test patch:
diff --git a/src/test/java/io/github/hectorvent/floci/services/s3/S3ReplicationIntegrationTest.java b/src/test/java/io/github/hectorvent/floci/services/s3/S3ReplicationIntegrationTest.java
new file mode 100644
index 0000000000..ca277ac17b
--- /dev/null
+++ b/src/test/java/io/github/hectorvent/floci/services/s3/S3ReplicationIntegrationTest.java
@@ -0,0 +1,86 @@
+package io.github.hectorvent.floci.services.s3;
+
+import io.quarkus.test.junit.QuarkusTest;
+import org.junit.jupiter.api.MethodOrderer;
+import org.junit.jupiter.api.Order;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestMethodOrder;
+
+import static io.restassured.RestAssured.given;
+import static org.hamcrest.Matchers.containsString;
+
+@QuarkusTest
+@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
+class S3ReplicationIntegrationTest {
+
+    private static final String BUCKET = "replication-int-test";
+
+    @Test
+    @Order(1)
+    void createBucket() {
+        given()
+        .when()
+            .put("/" + BUCKET)
+        .then()
+            .statusCode(200);
+    }
+
+    /**
+     * Regression test for the bucket-destroying bug where {@code DELETE /{bucket}?replication}
+     * (DeleteBucketReplication) was not handled and fell through to the unqualified
+     * {@code DeleteBucket}, silently deleting the entire bucket. Real S3 removes only the
+     * replication configuration and returns 204.
+     */
+    @Test
+    @Order(2)
+    void deleteReplicationDoesNotDeleteBucket() {
+        given()
+        .when()
+            .delete("/" + BUCKET + "?replication")
+        .then()
+            .statusCode(204);
+    }
+
+    @Test
+    @Order(3)
+    void bucketStillExistsAfterReplicationDelete() {
+        // A sub-resource-qualified DELETE must never remove the bucket itself.
+        given()
+        .when()
+            .get("/" + BUCKET + "?versioning")
+        .then()
+            .statusCode(200);
+    }
+
+    @Test
+    @Order(4)
+    void putVersioningAfterReplicationDeleteSucceeds() {
+        given()
+            .body("""
+                    <VersioningConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
+                        <Status>Enabled</Status>
+                    </VersioningConfiguration>
+                    """)
+        .when()
+            .put("/" + BUCKET + "?versioning")
+        .then()
+            .statusCode(200);
+    }
+
+    @Test
+    @Order(5)
+    void unqualifiedDeleteStillRemovesBucket() {
+        given()
+        .when()
+            .delete("/" + BUCKET)
+        .then()
+            .statusCode(204);
+        // Bucket is gone now: a sub-resource GET should report NoSuchBucket.
+        given()
+        .when()
+            .get("/" + BUCKET + "?versioning")
+        .then()
+            .statusCode(404)
+            .body(containsString("NoSuchBucket"));
+    }
+}