Skip to content

Commit ea50ddf

Browse files
committed
Accept unquoted etags in if-match/if-none-match headers
Fixes #2665
1 parent 3e52489 commit ea50ddf

File tree

3 files changed

+50
-4
lines changed

3 files changed

+50
-4
lines changed

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,8 @@ Version 4.x is JDK17 LTS bytecode compatible, with Docker and JUnit / direct Jav
172172
Version 4.x is JDK17 LTS bytecode compatible, with Docker and JUnit / direct Java integration.
173173

174174
* Features and fixes
175-
* TBD
175+
* Return correct error body on invalid ranges (fixes #2732)
176+
* Accept unquoted etags in if-match/if-none-match headers (fixes #2665)
176177
* Refactorings
177178
* TBD
178179
* Version updates (deliverable dependencies)
@@ -183,6 +184,7 @@ Version 4.x is JDK17 LTS bytecode compatible, with Docker and JUnit / direct Jav
183184
* Bump org.apache.commons:commons-lang3 from 3.18.0 to 3.19.0
184185
* Version updates (build dependencies)
185186
* Bump kotlin.version from 2.2.20 to 2.2.21
187+
* Bump aws.sdk.kotlin:s3-jvm from 1.5.41 to 1.5.73
186188
* Bump digital.pragmatech.testing:spring-test-profiler from 0.0.12 to 0.0.13
187189
* Bump org.mockito.kotlin:mockito-kotlin from 6.0.0 to 6.1.0
188190
* Bump org.xmlunit:xmlunit-assertj3 from 2.10.4 to 2.11.0

integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/GetPutDeleteObjectIT.kt

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1336,6 +1336,23 @@ internal class GetPutDeleteObjectIT : S3TestBase() {
13361336
}
13371337
}
13381338

1339+
@Test
1340+
@S3VerifiedTodo
1341+
fun `GET object succeeds with unquoted if-match=true`(testInfo: TestInfo) {
1342+
val (bucketName, putObjectResponse) = givenBucketAndObject(testInfo, UPLOAD_FILE_NAME)
1343+
val matchingEtag = putObjectResponse.eTag()
1344+
val unquotedEtag = matchingEtag.substring(1, matchingEtag.length - 1)
1345+
s3Client
1346+
.getObject {
1347+
it.bucket(bucketName)
1348+
it.key(UPLOAD_FILE_NAME)
1349+
it.ifMatch(unquotedEtag)
1350+
}.use {
1351+
assertThat(it.response().eTag()).isEqualTo(matchingEtag)
1352+
assertThat(it.response().contentLength()).isEqualTo(UPLOAD_FILE_LENGTH)
1353+
}
1354+
}
1355+
13391356
@Test
13401357
@S3VerifiedSuccess(year = 2025)
13411358
fun `GET object succeeds with if-match=true and if-unmodified-since=false`(testInfo: TestInfo) {
@@ -1407,6 +1424,23 @@ internal class GetPutDeleteObjectIT : S3TestBase() {
14071424
.hasMessageContaining("Service: S3, Status Code: 304")
14081425
}
14091426

1427+
@Test
1428+
@S3VerifiedTodo
1429+
fun `GET object fails with unquoted if-none-match=false`(testInfo: TestInfo) {
1430+
val (bucketName, putObjectResponse) = givenBucketAndObject(testInfo, UPLOAD_FILE_NAME)
1431+
val matchingEtag = putObjectResponse.eTag()
1432+
val unquotedEtag = matchingEtag.substring(1, matchingEtag.length - 1)
1433+
1434+
assertThatThrownBy {
1435+
s3Client.getObject {
1436+
it.bucket(bucketName)
1437+
it.key(UPLOAD_FILE_NAME)
1438+
it.ifNoneMatch(unquotedEtag)
1439+
}
1440+
}.isInstanceOf(S3Exception::class.java)
1441+
.hasMessageContaining("Service: S3, Status Code: 304")
1442+
}
1443+
14101444
@Test
14111445
@S3VerifiedSuccess(year = 2025)
14121446
fun `GET object succeeds with if-modified-since=true`(testInfo: TestInfo) {

server/src/main/java/com/adobe/testing/s3mock/service/ObjectService.java

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -368,11 +368,16 @@ public void verifyObjectMatching(
368368

369369
var setMatch = match != null && !match.isEmpty();
370370
if (setMatch) {
371-
if (match.contains(WILDCARD_ETAG) || match.contains(WILDCARD) || match.contains(etag)) {
371+
var unquotedEtag = etag.replace("\"", "");
372+
if (match.contains(WILDCARD_ETAG)
373+
|| match.contains(WILDCARD)
374+
|| match.contains(etag)
375+
|| match.contains(unquotedEtag)
376+
) {
372377
// request cares only that the object exists or that the etag matches.
373378
LOG.debug("Object {} exists", s3ObjectMetadata.key());
374379
return;
375-
} else if (!match.contains(etag)) {
380+
} else if (!match.contains(unquotedEtag) && !match.contains(etag)) {
376381
LOG.debug("Object {} does not match etag {}", s3ObjectMetadata.key(), etag);
377382
throw PRECONDITION_FAILED;
378383
}
@@ -388,7 +393,12 @@ public void verifyObjectMatching(
388393

389394
var setNoneMatch = noneMatch != null && !noneMatch.isEmpty();
390395
if (setNoneMatch) {
391-
if (noneMatch.contains(WILDCARD_ETAG) || noneMatch.contains(WILDCARD) || noneMatch.contains(etag)) {
396+
var unquotedEtag = etag.replace("\"", "");
397+
if (noneMatch.contains(WILDCARD_ETAG)
398+
|| noneMatch.contains(WILDCARD)
399+
|| noneMatch.contains(etag)
400+
|| noneMatch.contains(unquotedEtag)
401+
) {
392402
// request cares only that the object etag does not match.
393403
LOG.debug("Object {} has an ETag {} that matches one of the 'noneMatch' values", s3ObjectMetadata.key(), etag);
394404
throw NOT_MODIFIED;

0 commit comments

Comments
 (0)