Thanks to visit codestin.com
Credit goes to github.com

Skip to content

ETag quoting mismatch between AWS S3 and S3Mock when using conditional headers #2665

@AlejandroBlanco

Description

@AlejandroBlanco

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:

GET https://localhost:9090/staging?list-type=2&delimiter=%2F&max-keys=2&prefix=grades.csv%2F&fetch-owner=false

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

Metadata

Metadata

Assignees

Labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions