diff --git a/CHANGELOG.md b/CHANGELOG.md
index 78660edb2..5d08e7901 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,7 +9,7 @@ Whenever a 3rd party library is updated, S3Mock will update it's MINOR version.
* [Planned changes](#planned-changes)
* [CURRENT - 4.x - THIS VERSION IS UNDER ACTIVE DEVELOPMENT](#current---4x---this-version-is-under-active-development)
* [4.11.0 - PLANNED](#4110---planned)
- * [4.10.0 - PLANNED](#4100---planned)
+ * [4.10.0](#4100)
* [4.9.1](#491)
* [4.9.0](#490)
* [4.8.0](#480)
@@ -155,7 +155,7 @@ Version 4.x is JDK17 LTS bytecode compatible, with Docker and JUnit / direct Jav
## 4.11.0 - PLANNED
Version 4.x is JDK17 LTS bytecode compatible, with Docker and JUnit / direct Java integration.
-**This is the last currently planned minor release of 4.x.**
+**This is currently the last planned minor release of 4.x.**
* Features and fixes
* TBD
@@ -164,23 +164,42 @@ Version 4.x is JDK17 LTS bytecode compatible, with Docker and JUnit / direct Jav
* Version updates (deliverable dependencies)
* Update to Spring Boot 3.5.8
* Planned release November 20th 2025
- * TBD: link to milestone
+ * https://github.com/spring-projects/spring-boot/milestone/401
* Version updates (build dependencies)
* TBD
-## 4.10.0 - PLANNED
+## 4.10.0
Version 4.x is JDK17 LTS bytecode compatible, with Docker and JUnit / direct Java integration.
* Features and fixes
- * TBD
+ * Return correct error body on invalid ranges (fixes #2732)
+ * Accept unquoted etags in if-match/if-none-match headers (fixes #2665)
* Refactorings
- * TBD
+ * Drop commons-lang3 dependency and replace its usages with core Java (fixes #2735)
* Version updates (deliverable dependencies)
- * Update to Spring Boot 3.5.7
- * Planned release October 23rd 2025
- * https://github.com/spring-projects/spring-boot/milestone/399
+ * Bump spring-boot.version from 3.5.6 to 3.5.7
+ * Bump aws-v2.version from 2.33.12 to 2.37.2
+ * Bump aws.version from 1.12.791 to 1.12.793
+ * Bump alpine from 3.22.1 to 3.22.2 in /docker
+ * Bump org.apache.commons:commons-lang3 from 3.18.0 to 3.19.0
* Version updates (build dependencies)
- * TBD
+ * Bump kotlin.version from 2.2.20 to 2.2.21
+ * Bump aws.sdk.kotlin:s3-jvm from 1.5.41 to 1.5.73
+ * Bump digital.pragmatech.testing:spring-test-profiler from 0.0.12 to 0.0.13
+ * Bump org.mockito.kotlin:mockito-kotlin from 6.0.0 to 6.1.0
+ * Bump org.xmlunit:xmlunit-assertj3 from 2.10.4 to 2.11.0
+ * Bump org.codehaus.mojo:exec-maven-plugin from 3.5.1 to 3.6.2
+ * Bump org.apache.maven.plugins:maven-javadoc-plugin from 3.11.3 to 3.12.0
+ * Bump org.apache.maven.plugins:maven-compiler-plugin from 3.14.0 to 3.14.1
+ * Bump org.apache.maven.plugins:maven-dependency-plugin from 3.8.1 to 3.9.0
+ * Bump org.apache.maven.plugins:maven-enforcer-plugin from 3.6.1 to 3.6.2
+ * Bump com.puppycrawl.tools:checkstyle from 11.0.1 to 12.1.1
+ * Bump org.jacoco:jacoco-maven-plugin from 0.8.13 to 0.8.14
+ * Bump github/codeql-action from 3.30.3 to 4.31.2
+ * Bump actions/dependency-review-action from 4.7.3 to 4.8.1
+ * Bump ossf/scorecard-action from 2.4.2 to 2.4.3
+ * Bump actions/stale from 10.0.0 to 10.1.0
+ * Bump actions/upload-artifact from 4.6.2 to 5.0.0
## 4.9.1
Version 4.x is JDK17 LTS bytecode compatible, with Docker and JUnit / direct Java integration.
diff --git a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/GetPutDeleteObjectIT.kt b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/GetPutDeleteObjectIT.kt
index 160eb45e0..4e7e1f3f3 100644
--- a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/GetPutDeleteObjectIT.kt
+++ b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/GetPutDeleteObjectIT.kt
@@ -1336,6 +1336,23 @@ internal class GetPutDeleteObjectIT : S3TestBase() {
}
}
+ @Test
+ @S3VerifiedTodo
+ fun `GET object succeeds with unquoted if-match=true`(testInfo: TestInfo) {
+ val (bucketName, putObjectResponse) = givenBucketAndObject(testInfo, UPLOAD_FILE_NAME)
+ val matchingEtag = putObjectResponse.eTag()
+ val unquotedEtag = matchingEtag.substring(1, matchingEtag.length - 1)
+ s3Client
+ .getObject {
+ it.bucket(bucketName)
+ it.key(UPLOAD_FILE_NAME)
+ it.ifMatch(unquotedEtag)
+ }.use {
+ assertThat(it.response().eTag()).isEqualTo(matchingEtag)
+ assertThat(it.response().contentLength()).isEqualTo(UPLOAD_FILE_LENGTH)
+ }
+ }
+
@Test
@S3VerifiedSuccess(year = 2025)
fun `GET object succeeds with if-match=true and if-unmodified-since=false`(testInfo: TestInfo) {
@@ -1407,6 +1424,23 @@ internal class GetPutDeleteObjectIT : S3TestBase() {
.hasMessageContaining("Service: S3, Status Code: 304")
}
+ @Test
+ @S3VerifiedTodo
+ fun `GET object fails with unquoted if-none-match=false`(testInfo: TestInfo) {
+ val (bucketName, putObjectResponse) = givenBucketAndObject(testInfo, UPLOAD_FILE_NAME)
+ val matchingEtag = putObjectResponse.eTag()
+ val unquotedEtag = matchingEtag.substring(1, matchingEtag.length - 1)
+
+ assertThatThrownBy {
+ s3Client.getObject {
+ it.bucket(bucketName)
+ it.key(UPLOAD_FILE_NAME)
+ it.ifNoneMatch(unquotedEtag)
+ }
+ }.isInstanceOf(S3Exception::class.java)
+ .hasMessageContaining("Service: S3, Status Code: 304")
+ }
+
@Test
@S3VerifiedSuccess(year = 2025)
fun `GET object succeeds with if-modified-since=true`(testInfo: TestInfo) {
diff --git a/pom.xml b/pom.xml
index b1b7aa59a..fc4dd77db 100644
--- a/pom.xml
+++ b/pom.xml
@@ -83,7 +83,7 @@
2.2
1.10.2
- 2.33.12
+ 2.37.2
1.12.793
diff --git a/server/src/main/java/com/adobe/testing/s3mock/service/ObjectService.java b/server/src/main/java/com/adobe/testing/s3mock/service/ObjectService.java
index 8d2dab691..a2693613b 100644
--- a/server/src/main/java/com/adobe/testing/s3mock/service/ObjectService.java
+++ b/server/src/main/java/com/adobe/testing/s3mock/service/ObjectService.java
@@ -368,11 +368,16 @@ public void verifyObjectMatching(
var setMatch = match != null && !match.isEmpty();
if (setMatch) {
- if (match.contains(WILDCARD_ETAG) || match.contains(WILDCARD) || match.contains(etag)) {
+ var unquotedEtag = etag.replace("\"", "");
+ if (match.contains(WILDCARD_ETAG)
+ || match.contains(WILDCARD)
+ || match.contains(etag)
+ || match.contains(unquotedEtag)
+ ) {
// request cares only that the object exists or that the etag matches.
LOG.debug("Object {} exists", s3ObjectMetadata.key());
return;
- } else if (!match.contains(etag)) {
+ } else if (!match.contains(unquotedEtag) && !match.contains(etag)) {
LOG.debug("Object {} does not match etag {}", s3ObjectMetadata.key(), etag);
throw PRECONDITION_FAILED;
}
@@ -388,7 +393,12 @@ public void verifyObjectMatching(
var setNoneMatch = noneMatch != null && !noneMatch.isEmpty();
if (setNoneMatch) {
- if (noneMatch.contains(WILDCARD_ETAG) || noneMatch.contains(WILDCARD) || noneMatch.contains(etag)) {
+ var unquotedEtag = etag.replace("\"", "");
+ if (noneMatch.contains(WILDCARD_ETAG)
+ || noneMatch.contains(WILDCARD)
+ || noneMatch.contains(etag)
+ || noneMatch.contains(unquotedEtag)
+ ) {
// request cares only that the object etag does not match.
LOG.debug("Object {} has an ETag {} that matches one of the 'noneMatch' values", s3ObjectMetadata.key(), etag);
throw NOT_MODIFIED;
diff --git a/server/src/test/kotlin/com/adobe/testing/s3mock/ChecksumTestUtil.kt b/server/src/test/kotlin/com/adobe/testing/s3mock/ChecksumTestUtil.kt
index 1a48cf903..b9951b532 100644
--- a/server/src/test/kotlin/com/adobe/testing/s3mock/ChecksumTestUtil.kt
+++ b/server/src/test/kotlin/com/adobe/testing/s3mock/ChecksumTestUtil.kt
@@ -28,6 +28,7 @@ import software.amazon.awssdk.http.auth.aws.internal.signer.chunkedencoding.Trai
import software.amazon.awssdk.http.auth.aws.internal.signer.io.ChecksumInputStream
import software.amazon.awssdk.http.auth.aws.internal.signer.util.ChecksumUtil
import software.amazon.awssdk.http.auth.aws.internal.signer.util.SignerUtils
+import software.amazon.awssdk.http.auth.spi.signer.PayloadChecksumStore
import software.amazon.awssdk.identity.spi.AwsCredentialsIdentity
import java.io.File
import java.io.InputStream
@@ -85,7 +86,12 @@ object ChecksumTestUtil {
mutableSetOf(sdkChecksum)
)
- val checksumTrailer: TrailerProvider = ChecksumTrailerProvider(sdkChecksum, checksumHeaderName)
+ val checksumTrailer: TrailerProvider = ChecksumTrailerProvider(
+ sdkChecksum,
+ checksumHeaderName,
+ checksumAlgorithm,
+ PayloadChecksumStore.create()
+ )
builder.inputStream(checksumInputStream).addTrailer(checksumTrailer)
}