From 7b8b6eb4817df18e2329c20a4b7ae31f9cb25c90 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 10 Apr 2026 10:02:05 +0000 Subject: [PATCH 1/3] fix(s3): return XML error responses for presigned POST failures Presigned POST errors (e.g. policy condition failures) were being thrown as AwsException and caught by the global AwsExceptionMapper, which returns JSON. Real AWS S3 and LocalStack return XML error responses for presigned POST operations. Wrap handlePresignedPost in a try/catch that converts AwsException to XML via xmlErrorResponse(), matching the AWS/LocalStack behavior. Co-Authored-By: Matej Snuderl --- .../hectorvent/floci/services/s3/S3Controller.java | 10 ++++++++++ 1 file changed, 10 insertions(+) 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 e913e85f..b9a9bc20 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 @@ -1590,6 +1590,16 @@ private Instant parseHttpDate(String dateStr) { } private Response handlePresignedPost(String bucket, String contentType, byte[] body) { + try { + return doHandlePresignedPost(bucket, contentType, body); + } catch (AwsException e) { + // Presigned POST errors must be returned as XML (matching LocalStack/AWS), + // not JSON which is what the global AwsExceptionMapper would produce. + return xmlErrorResponse(e); + } + } + + private Response doHandlePresignedPost(String bucket, String contentType, byte[] body) { String boundary = extractBoundary(contentType); if (boundary == null) { throw new AwsException("InvalidArgument", From 5599f03442a572651ec6d87e133513515d289ef8 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 10 Apr 2026 10:25:21 +0000 Subject: [PATCH 2/3] test(s3): add raw XML wire format assertion for presigned POST errors Adds a test that extracts the raw response body and asserts it contains the exact XML substring that downstream consumers (e.g. seadn) expect: AccessDeniedInvalid according to Policy: ... ["eq", "$Content-Type", "image/png"] This complements the existing hasXPath-based assertions which auto- unescape XML entities and therefore don't verify the wire format. Co-Authored-By: Matej Snuderl --- .../s3/S3PresignedPostIntegrationTest.java | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/test/java/io/github/hectorvent/floci/services/s3/S3PresignedPostIntegrationTest.java b/src/test/java/io/github/hectorvent/floci/services/s3/S3PresignedPostIntegrationTest.java index fc0e733f..76846e2c 100644 --- a/src/test/java/io/github/hectorvent/floci/services/s3/S3PresignedPostIntegrationTest.java +++ b/src/test/java/io/github/hectorvent/floci/services/s3/S3PresignedPostIntegrationTest.java @@ -13,6 +13,7 @@ import java.util.Base64; import static io.restassured.RestAssured.given; +import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.*; @QuarkusTest @@ -376,6 +377,43 @@ void presignedPostRejectsStartsWithMismatch() { + "[\"starts-with\", \"$key\", \"uploads/\"]"))); } + @Test + @Order(94) + void presignedPostReturnsXmlErrorResponseBody() { + // Verify the raw XML wire format matches what AWS S3 and LocalStack return. + // This ensures clients that parse the raw response body (e.g. seadn) see the + // expected XML structure with "-encoded quotes, not JSON. + String key = "uploads/xml-error-check.png"; + String fileContent = "not a real png"; + + String policy = buildPolicy(BUCKET, key, "image/png", 0, 10485760); + String policyBase64 = Base64.getEncoder().encodeToString(policy.getBytes(StandardCharsets.UTF_8)); + + String responseBody = + given() + .multiPart("key", key) + .multiPart("Content-Type", "image/gif") + .multiPart("policy", policyBase64) + .multiPart("x-amz-algorithm", "AWS4-HMAC-SHA256") + .multiPart("x-amz-credential", "AKIAIOSFODNN7EXAMPLE/20260330/us-east-1/s3/aws4_request") + .multiPart("x-amz-date", AMZ_DATE_FORMAT.format(Instant.now())) + .multiPart("x-amz-signature", "dummysignature") + .multiPart("file", "xml-error-check.png", fileContent.getBytes(StandardCharsets.UTF_8), "image/gif") + .when() + .post("/" + BUCKET) + .then() + .statusCode(403) + .contentType("application/xml") + .extract().body().asString(); + + // Verify the raw XML contains the expected structure with XML-escaped quotes. + // This is the exact substring that the seadn test suite asserts against. + assertThat(responseBody, containsString( + "AccessDenied" + + "Invalid according to Policy: Policy Condition failed: " + + "["eq", "$Content-Type", "image/png"]")); + } + @Test @Order(95) void presignedPostEnforcesPolicyWithCapitalPFieldName() { From eb77dad4b89211e1fa714ad13dff50dd161bb177 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 10 Apr 2026 10:26:59 +0000 Subject: [PATCH 3/3] refactor(test): assert exact XML response with regex for RequestId Replace containsString with matchesRegex to assert the full XML response structure. The only dynamic part is RequestId (random UUID), matched with a regex pattern. Co-Authored-By: Matej Snuderl --- .../s3/S3PresignedPostIntegrationTest.java | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/test/java/io/github/hectorvent/floci/services/s3/S3PresignedPostIntegrationTest.java b/src/test/java/io/github/hectorvent/floci/services/s3/S3PresignedPostIntegrationTest.java index 76846e2c..739ae1d6 100644 --- a/src/test/java/io/github/hectorvent/floci/services/s3/S3PresignedPostIntegrationTest.java +++ b/src/test/java/io/github/hectorvent/floci/services/s3/S3PresignedPostIntegrationTest.java @@ -406,12 +406,16 @@ void presignedPostReturnsXmlErrorResponseBody() { .contentType("application/xml") .extract().body().asString(); - // Verify the raw XML contains the expected structure with XML-escaped quotes. - // This is the exact substring that the seadn test suite asserts against. - assertThat(responseBody, containsString( - "AccessDenied" - + "Invalid according to Policy: Policy Condition failed: " - + "["eq", "$Content-Type", "image/png"]")); + // Assert the exact XML structure, matching what AWS S3 and LocalStack return. + // The RequestId is a random UUID, so we match it with a regex. + assertThat(responseBody, matchesRegex( + "\\Q\\E" + + "\\Q\\E" + + "\\QAccessDenied\\E" + + "\\QInvalid according to Policy: Policy Condition failed: \\E" + + "\\Q["eq", "$Content-Type", "image/png"]\\E" + + "[0-9a-f\\-]+" + + "\\Q\\E")); } @Test