-
Couldn't load subscription status.
- Fork 189
Description
Description
We've observed a difference in behaviour between AWS S3 and S3Mock regarding how ETags are handled in conditional requests. S3Mock would appear to be in line with the RFC 7232, but the AWS SDK client aws-java-sdk-1.12.367.jar (and later versions) has slightly different behaviour in how to treat etags
Etag is stored internally in each Adobe S3Mock object metadata as a quoted String (i.e. ""abcde"") and clients are expected to send their queries with quoted etags to reference them. Testing has shown that AWS S3 client in aws-java-sdk jar are sending etags in the If-Match header, If-None-Match and If-Range unquoted and that AWS S3 can process them as if they were quoted while Adobe S3Mock fails to match the etags and will return a 412 PRECONDITION FAILED HTTP error. We had implemented an IT test proving this matter. We believe S3Mock should behave as AWS S3 and that therefore some changes would require to be introduced.
Steps to Reproduce
Create a bucket, i.e: curl --request PUT "http://localhost:9090/staging"
Upload an object to S3Mock, i.e: curl --request PUT --upload-file example/grades.csv "http://localhost:9090/staging/grades.csv"
S3Mock stores the ETag internally as a quoted string
Perform a GetObject request with the AWS Java SDK with If-Match. An IT is attached:
@test
@S3VerifiedSuccess(year = 2025)
fun GET object succeeds with unquoted if-match header(testInfo: TestInfo) {
val (bucketName, putObjectResponse) = givenBucketAndObject(testInfo, MYFILE)
val matchingEtag = putObjectResponse.eTag()
val unquotedEtag = matchingEtag.substring(1,matchingEtag.length - 1)
s3Client.getObject {
it.bucket(bucketName)
it.key(MYFILE)
it.ifMatch(unquotedEtag)
}.use {
assertThat(it.response().eTag()).isEqualTo(matchingEtag)
assertThat(it.response().contentLength()).isEqualTo(MYFILE_LENGTH)
}
}
This test will cause two rounds (among others for setup) of messages between client and s3mock ending in failure:
The first one to get the object metadata with a GET:
It returns the object metadata:
<ListBucketResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<Delimiter>%2F</Delimiter>
<IsTruncated>false</IsTruncated>
<KeyCount>0</KeyCount>
<MaxKeys>2</MaxKeys>
<Name>staging</Name>
<Prefix>grades.csv%2F</Prefix>
</ListBucketResult>
A HEAD to retrieve the etag of the object: HEAD http://localhost:9090/staging/grades.csv
The response includes a header with a quoted tag
Finally, a GET again including the UNQUOTED etag in the header: GET http://localhost:9090/staging/grades.csv
- S3Mock will return a 412 PRECONDITION error
<Error>
<Code>PreconditionFailed</Code>
<Message> At least one of the pre-conditions you specified did not hold</Message>
</Error>
- Same call to AWS S3 returns a 200 with the correct file
HTTP/1.1 206
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
Accept-Ranges: bytes
Content-Range: bytes 0-1544/1545
ETag: "c96bc0a93bf7697b94f07fe964944c12"
Last-Modified: Wed, 01 Oct 2025 09:28:58 GMT
Content-Type: application/octet-stream
Content-Length: 1545
Date: Wed, 01 Oct 2025 11:28:17 GMT
Keep-Alive: timeout=60
Connection: keep-alive
...File contents...
Expected Behaviour
Strictly speaking, S3Mock is as per the spec but we'd like to ensure the behaviour was as per AWS S3.
Impact
Most calls from aws-java-sdk.jar, or aws-java-sdk-s3.jar, will fail as GET, PUT and DELETEs will make use of If-Match and If-None-Match with unquoted etags
Suggested Fix
Let quotes and unquoted be processed as AWS S3 does. We have a fix for this issue on file ObjectService.java. We can create a PR and collaborate on Adobe S3Mock.
Add-ons
Incidentally we have discovered an IT test which is disabled because a failure the Adobe S3Mock team couldn't find. We have detected the test named "HEAD object succeeds with if-match=true and if-unmodified-since=false" was meant to test this case however it fails because the if-unmodified header is not properly parser, causing a 412 error too but not because the etag is not match (it won't match) but because the function processing timestamps in the test is not parsing the time properly
org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [@org.springframework.web.bind.annotation.RequestHeader java.time.Instant] for value [Fri]
Fixing the parsing function would still cause a 412 until our fix is applied