Repository: fsouza/fake-gcs-server (go)

Issue:
etag value has extra backslashes when uploading object
When uploading an object against a `fake-gcs-server`, I'm noticing that the `etag` value has escaped backslashes before and after the actual md5 hash. Looks to be due to poor escaping. A GCS client library that I'm using fails to parse the `etag` attribute because of this. I wasn't able to repro when uploading against GCS itself.

Raw json output when uploading against `fake-gcs-server`:
```
"... , \"etag\": \"\\\"1B2M2Y8AsgTpgAmY7PhCfg==\\\"\", ..."
```

Raw json output when uploading against GCS itself:
```
"... , \"etag\": \"TKb9iteyqIEDEEE=\", ..."
```

Pull request description:
Remove unnecessary quoting around ETag in JSON response
The JSON marshaller will already escape the value appropriately. Quoting the value explicitly beforehand is counterproductive, as the quotes introduced in that step would be considered legitimate part of the ETag value and would be themselves escaped when marshalling.

When setting the `ETag` HTTP response header, however, we do have to explicitly add the quoting. This is because double quotes are not added automatically for HTTP headers and, according to the [standard][1], the `ETag` header value _must_ be wrapped in double quotes.

[1]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag

Fixes #1327

Patch:
diff --git a/.github/workflows/docker-push.yml b/.github/workflows/docker-push.yml
index 55d5165..a5fee7f 100644
--- a/.github/workflows/docker-push.yml
+++ b/.github/workflows/docker-push.yml
@@ -10,7 +10,7 @@ jobs:
   build-and-push:
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v4.1.1
+      - uses: actions/checkout@v4.1.2
 
       - name: docker meta
         id: meta
@@ -32,13 +32,13 @@ jobs:
         uses: docker/setup-buildx-action@v3.1.0
 
       - name: login to docker hub
-        uses: docker/login-action@v3.0.0
+        uses: docker/login-action@v3.1.0
         with:
           username: ${{ secrets.DOCKER_USERNAME }}
           password: ${{ secrets.DOCKER_PASSWORD }}
 
       - name: build and push
-        uses: docker/build-push-action@v5.1.0
+        uses: docker/build-push-action@v5.2.0
         with:
           cache-from: fsouzaci/gha-cache:fake-gcs-server
           cache-to: fsouzaci/gha-cache:fake-gcs-server
diff --git a/.github/workflows/goreleaser-run.yml b/.github/workflows/goreleaser-run.yml
index f8e68cd..009a9ec 100644
--- a/.github/workflows/goreleaser-run.yml
+++ b/.github/workflows/goreleaser-run.yml
@@ -11,7 +11,7 @@ jobs:
   release:
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v4.1.1
+      - uses: actions/checkout@v4.1.2
         with:
           fetch-depth: 0
 
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 48ae17e..8fe6bdf 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -29,7 +29,7 @@ jobs:
     name: tests (${{ matrix.os }}/go-${{ matrix.go_version }}-${{ matrix.arch }})
     runs-on: ${{ matrix.os }}-latest
     steps:
-      - uses: actions/checkout@v4.1.1
+      - uses: actions/checkout@v4.1.2
 
       - uses: actions/setup-go@v5.0.0
         id: go
@@ -52,7 +52,7 @@ jobs:
     name: lint
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v4.1.1
+      - uses: actions/checkout@v4.1.2
 
       - uses: golangci/golangci-lint-action@v4.0.0
 
@@ -60,7 +60,7 @@ jobs:
     name: staticcheck
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v4.1.1
+      - uses: actions/checkout@v4.1.2
 
       - uses: actions/setup-go@v5.0.0
         id: go
@@ -90,7 +90,7 @@ jobs:
     name: build
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v4.1.1
+      - uses: actions/checkout@v4.1.2
 
       - uses: actions/setup-go@v5.0.0
         id: go
@@ -163,7 +163,7 @@ jobs:
       - lint
       - staticcheck
     steps:
-      - uses: actions/checkout@v4.1.1
+      - uses: actions/checkout@v4.1.2
 
       - name: download binary
         uses: actions/download-artifact@v4.1.4
diff --git a/examples/go/go.mod b/examples/go/go.mod
index 627f220..911ef42 100644
--- a/examples/go/go.mod
+++ b/examples/go/go.mod
@@ -3,12 +3,12 @@ module github.com/fsouza/fake-gcs-server/examples/go-example
 go 1.22
 
 require (
-	cloud.google.com/go/storage v1.39.0
+	cloud.google.com/go/storage v1.39.1
 	google.golang.org/api v0.168.0
 )
 
 require (
-	cloud.google.com/go v0.112.0 // indirect
+	cloud.google.com/go v0.112.1 // indirect
 	cloud.google.com/go/compute v1.24.0 // indirect
 	cloud.google.com/go/compute/metadata v0.2.3 // indirect
 	cloud.google.com/go/iam v1.1.6 // indirect
@@ -36,8 +36,8 @@ require (
 	golang.org/x/time v0.5.0 // indirect
 	google.golang.org/appengine v1.6.8 // indirect
 	google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 // indirect
-	google.golang.org/genproto/googleapis/api v0.0.0-20240221002015-b0ce06bbee7c // indirect
+	google.golang.org/genproto/googleapis/api v0.0.0-20240304161311-37d4d3c04a78 // indirect
 	google.golang.org/genproto/googleapis/rpc v0.0.0-20240304161311-37d4d3c04a78 // indirect
 	google.golang.org/grpc v1.62.0 // indirect
-	google.golang.org/protobuf v1.32.0 // indirect
+	google.golang.org/protobuf v1.33.0 // indirect
 )
diff --git a/examples/go/go.sum b/examples/go/go.sum
index 09c065d..61d3393 100644
--- a/examples/go/go.sum
+++ b/examples/go/go.sum
@@ -1,14 +1,14 @@
 cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
-cloud.google.com/go v0.112.0 h1:tpFCD7hpHFlQ8yPwT3x+QeXqc2T6+n6T+hmABHfDUSM=
-cloud.google.com/go v0.112.0/go.mod h1:3jEEVwZ/MHU4djK5t5RHuKOA/GbLddgTdVubX1qnPD4=
+cloud.google.com/go v0.112.1 h1:uJSeirPke5UNZHIb4SxfZklVSiWWVqW4oXlETwZziwM=
+cloud.google.com/go v0.112.1/go.mod h1:+Vbu+Y1UU+I1rjmzeMOb/8RfkKJK2Gyxi1X6jJCZLo4=
 cloud.google.com/go/compute v1.24.0 h1:phWcR2eWzRJaL/kOiJwfFsPs4BaKq1j6vnpZrc1YlVg=
 cloud.google.com/go/compute v1.24.0/go.mod h1:kw1/T+h/+tK2LJK0wiPPx1intgdAM3j/g3hFDlscY40=
 cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
 cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
 cloud.google.com/go/iam v1.1.6 h1:bEa06k05IO4f4uJonbB5iAgKTPpABy1ayxaIZV/GHVc=
 cloud.google.com/go/iam v1.1.6/go.mod h1:O0zxdPeGBoFdWW3HWmBxJsk0pfvNM/p/qa82rWOGTwI=
-cloud.google.com/go/storage v1.39.0 h1:brbjUa4hbDHhpQf48tjqMaXEV+f1OGoaTmQau9tmCsA=
-cloud.google.com/go/storage v1.39.0/go.mod h1:OAEj/WZwUYjA3YHQ10/YcN9ttGuEpLwvaoyBXIPikEk=
+cloud.google.com/go/storage v1.39.1 h1:MvraqHKhogCOTXTlct/9C3K3+Uy2jBmFYb3/Sp6dVtY=
+cloud.google.com/go/storage v1.39.1/go.mod h1:xK6xZmxZmo+fyP7+DEF6FhNc24/JAe95OLyOHCXFH1o=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
@@ -87,8 +87,8 @@ go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
 go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
 go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI=
 go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=
-go.opentelemetry.io/otel/sdk v1.21.0 h1:FTt8qirL1EysG6sTQRZ5TokkU8d0ugCj8htOgThZXQ8=
-go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E=
+go.opentelemetry.io/otel/sdk v1.22.0 h1:6coWHw9xw7EfClIC/+O31R8IY3/+EiRFHevmHafB2Gw=
+go.opentelemetry.io/otel/sdk v1.22.0/go.mod h1:iu7luyVGYovrRpe2fmj3CVKouQNdTOkxtLzPvPz1DOc=
 go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
 go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@@ -163,8 +163,8 @@ google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98
 google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
 google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 h1:9+tzLLstTlPTRyJTh+ah5wIMsBW5c4tQwGTN3thOW9Y=
 google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9/go.mod h1:mqHbVIp48Muh7Ywss/AD6I5kNVKZMmAa/QEW58Gxp2s=
-google.golang.org/genproto/googleapis/api v0.0.0-20240221002015-b0ce06bbee7c h1:9g7erC9qu44ks7UK4gDNlnk4kOxZG707xKm4jVniy6o=
-google.golang.org/genproto/googleapis/api v0.0.0-20240221002015-b0ce06bbee7c/go.mod h1:5iCWqnniDlqZHrd3neWVTOwvh/v6s3232omMecelax8=
+google.golang.org/genproto/googleapis/api v0.0.0-20240304161311-37d4d3c04a78 h1:SzXBGiWM1LNVYLCRP3e0/Gsze804l4jGoJ5lYysEO5I=
+google.golang.org/genproto/googleapis/api v0.0.0-20240304161311-37d4d3c04a78/go.mod h1:O1cOfN1Cy6QEYr7VxtjOyP5AdAuR0aJ/MYZaaof623Y=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20240304161311-37d4d3c04a78 h1:Xs9lu+tLXxLIfuci70nG4cpwaRC+mRQPUL7LoIeDJC4=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20240304161311-37d4d3c04a78/go.mod h1:UCOku4NytXMJuLQE5VuqA5lX3PcHCBo8pxNyvkf4xBs=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
@@ -185,8 +185,8 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD
 google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
-google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
+google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
+google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
diff --git a/examples/node/package-lock.json b/examples/node/package-lock.json
index a25c739..ea4687e 100644
--- a/examples/node/package-lock.json
+++ b/examples/node/package-lock.json
@@ -24,9 +24,9 @@
       "integrity": "sha512-Orxzlfb9c67A15cq2JQEyVc7wEsmFBmHjZWZYQMUyJ1qivXyMwdyNOs9odi79hze+2zqdTtu1E19IM/FtqZ10g=="
     },
     "@google-cloud/storage": {
-      "version": "7.7.0",
-      "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-7.7.0.tgz",
-      "integrity": "sha512-EMCEY+6JiIkx7Dt8NXVGGjy1vRdSGdHkoqZoqjJw7cEBkT7ZkX0c7puedfn1MamnzW5SX4xoa2jVq5u7OWBmkQ==",
+      "version": "7.8.0",
+      "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-7.8.0.tgz",
+      "integrity": "sha512-4q8rKdLp35z8msAtrhr0pbos7BeD8T0tr6rMbBINewp9cfrwj7ROIElVwBluU8fZ596OvwQcjb6QCyBzTmkMRQ==",
       "requires": {
         "@google-cloud/paginator": "^5.0.0",
         "@google-cloud/projectify": "^4.0.0",
@@ -34,11 +34,11 @@
         "abort-controller": "^3.0.0",
         "async-retry": "^1.3.3",
         "compressible": "^2.0.12",
-        "duplexify": "^4.0.0",
+        "duplexify": "^4.1.3",
         "ent": "^2.2.0",
         "fast-xml-parser": "^4.3.0",
         "gaxios": "^6.0.2",
-        "google-auth-library": "^9.0.0",
+        "google-auth-library": "^9.6.3",
         "mime": "^3.0.0",
         "mime-types": "^2.0.8",
         "p-limit": "^3.0.1",
@@ -58,9 +58,9 @@
       "integrity": "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg=="
     },
     "@types/node": {
-      "version": "20.10.1",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.1.tgz",
-      "integrity": "sha512-T2qwhjWwGH81vUEx4EXmBKsTJRXFXNZTL4v0gi01+zyBmCwzE6TyHszqX01m+QHTEq+EZNo13NeJIdEqf+Myrg==",
+      "version": "20.11.25",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.25.tgz",
+      "integrity": "sha512-TBHyJxk2b7HceLVGFcpAUjsa5zIdsPWlR6XHfyGzd0SFu+/NFgQgMAl96MSDZgQDvJAvV6BKsFOrt6zIL09JDw==",
       "requires": {
         "undici-types": "~5.26.4"
       }
@@ -160,14 +160,14 @@
       "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="
     },
     "duplexify": {
-      "version": "4.1.2",
-      "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.2.tgz",
-      "integrity": "sha512-fz3OjcNCHmRP12MJoZMPglx8m4rrFP8rovnk4vT8Fs+aonZoCwGg10dSsQsfP/E62eZcPTMSMP6686fu9Qlqtw==",
+      "version": "4.1.3",
+      "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz",
+      "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==",
       "requires": {
         "end-of-stream": "^1.4.1",
         "inherits": "^2.0.3",
         "readable-stream": "^3.1.1",
-        "stream-shift": "^1.0.0"
+        "stream-shift": "^1.0.2"
       }
     },
     "ecdsa-sig-formatter": {
@@ -202,9 +202,9 @@
       "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
     },
     "fast-xml-parser": {
-      "version": "4.3.2",
-      "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.3.2.tgz",
-      "integrity": "sha512-rmrXUXwbJedoXkStenj1kkljNF7ugn5ZjR9FJcwmCfcCbtOMDghPajbc+Tck6vE6F5XsDmx+Pr2le9fw8+pXBg==",
+      "version": "4.3.5",
+      "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.3.5.tgz",
+      "integrity": "sha512-sWvP1Pl8H03B8oFJpFR3HE31HUfwtX7Rlf9BNsvdpujD4n7WMhfmu8h9wOV2u+c1k0ZilTADhPqypzx2J690ZQ==",
       "requires": {
         "strnum": "^1.0.5"
       }
@@ -220,9 +220,9 @@
       }
     },
     "gaxios": {
-      "version": "6.1.1",
-      "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.1.1.tgz",
-      "integrity": "sha512-bw8smrX+XlAoo9o1JAksBwX+hi/RG15J+NTSxmNPIclKC3ZVK6C2afwY8OSdRvOK0+ZLecUJYtj2MmjOt3Dm0w==",
+      "version": "6.3.0",
+      "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.3.0.tgz",
+      "integrity": "sha512-p+ggrQw3fBwH2F5N/PAI4k/G/y1art5OxKpb2J2chwNNHM4hHuAOtivjPuirMF4KNKwTTUal/lPfL2+7h2mEcg==",
       "requires": {
         "extend": "^3.0.2",
         "https-proxy-agent": "^7.0.1",
@@ -240,22 +240,22 @@
       }
     },
     "google-auth-library": {
-      "version": "9.4.0",
-      "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.4.0.tgz",
-      "integrity": "sha512-Sn7w6p59lP3sHWPjLBc5OY6Y44CwbRrst5vAVpVhtSeMzTzZAGheyovaHDkcRRl6QeGl+H3a1GNiHLcLsCJTCw==",
+      "version": "9.6.3",
+      "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.6.3.tgz",
+      "integrity": "sha512-4CacM29MLC2eT9Cey5GDVK4Q8t+MMp8+OEdOaqD9MG6b0dOyLORaaeJMPQ7EESVgm/+z5EKYyFLxgzBJlJgyHQ==",
       "requires": {
         "base64-js": "^1.3.0",
         "ecdsa-sig-formatter": "^1.0.11",
-        "gaxios": "^6.0.0",
+        "gaxios": "^6.1.1",
         "gcp-metadata": "^6.1.0",
         "gtoken": "^7.0.0",
         "jws": "^4.0.0"
       }
     },
     "gtoken": {
-      "version": "7.0.1",
-      "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.0.1.tgz",
-      "integrity": "sha512-KcFVtoP1CVFtQu0aSk3AyAt2og66PFhZAlkUOuWKwzMLoulHXG5W5wE5xAnHb+yl3/wEFoqGW7/cDGMU8igDZQ==",
+      "version": "7.1.0",
+      "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz",
+      "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==",
       "requires": {
         "gaxios": "^6.0.0",
         "jws": "^4.0.0"
@@ -282,9 +282,9 @@
       }
     },
     "https-proxy-agent": {
-      "version": "7.0.2",
-      "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.2.tgz",
-      "integrity": "sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==",
+      "version": "7.0.4",
+      "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz",
+      "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==",
       "requires": {
         "agent-base": "^7.0.2",
         "debug": "4"
@@ -390,12 +390,11 @@
       "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg=="
     },
     "retry-request": {
-      "version": "7.0.1",
-      "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-7.0.1.tgz",
-      "integrity": "sha512-ZI6vJp9rfB71mrZpw+n9p/B6HCsd7QJlSEQftZ+xfJzr3cQ9EPGKw1FF0BnViJ0fYREX6FhymBD2CARpmsFciQ==",
+      "version": "7.0.2",
+      "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-7.0.2.tgz",
+      "integrity": "sha512-dUOvLMJ0/JJYEn8NrpOaGNE7X3vpI5XlZS/u0ANjqtcZVKnIxP7IgCFwrKTxENw29emmwug53awKtaMm4i9g5w==",
       "requires": {
         "@types/request": "^2.48.8",
-        "debug": "^4.1.1",
         "extend": "^3.0.2",
         "teeny-request": "^9.0.0"
       }
@@ -414,9 +413,9 @@
       }
     },
     "stream-shift": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz",
-      "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ=="
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz",
+      "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ=="
     },
     "string_decoder": {
       "version": "1.3.0",
diff --git a/examples/node/package.json b/examples/node/package.json
index 98438f7..e167b33 100644
--- a/examples/node/package.json
+++ b/examples/node/package.json
@@ -10,6 +10,6 @@
   "author": "",
   "license": "ISC",
   "dependencies": {
-    "@google-cloud/storage": "^7.7.0"
+    "@google-cloud/storage": "^7.8.0"
   }
 }
diff --git a/fakestorage/json_response.go b/fakestorage/json_response.go
index f16a7c5..99e8ce7 100644
--- a/fakestorage/json_response.go
+++ b/fakestorage/json_response.go
@@ -32,7 +32,7 @@ func jsonToHTTPHandler(h jsonHandler) http.HandlerFunc {
 		status := resp.getStatus()
 		var data any
 		if status > 399 {
-			data = newErrorResponse(status, resp.getErrorMessage(status), nil)
+			data = newErrorResponse(status, resp.getErrorMessage(status), resp.getErrorList(status))
 		} else {
 			data = resp.data
 		}
@@ -59,6 +59,18 @@ func (r *jsonResponse) getErrorMessage(status int) string {
 	return http.StatusText(status)
 }
 
+func (r *jsonResponse) getErrorList(status int) []apiError {
+	if status == http.StatusOK {
+		return nil
+	} else {
+		return []apiError{{
+			Domain:  "global",
+			Reason:  http.StatusText(status),
+			Message: r.getErrorMessage(status),
+		}}
+	}
+}
+
 func errToJsonResponse(err error) jsonResponse {
 	status := 0
 	var pathError *os.PathError
diff --git a/fakestorage/object.go b/fakestorage/object.go
index c433a62..c7b4095 100644
--- a/fakestorage/object.go
+++ b/fakestorage/object.go
@@ -854,7 +854,7 @@ func (s *Server) downloadObject(w http.ResponseWriter, r *http.Request) {
 	w.Header().Set("X-Goog-Generation", strconv.FormatInt(obj.Generation, 10))
 	w.Header().Set("X-Goog-Hash", fmt.Sprintf("crc32c=%s,md5=%s", obj.Crc32c, obj.Md5Hash))
 	w.Header().Set("Last-Modified", obj.Updated.Format(http.TimeFormat))
-	w.Header().Set("ETag", obj.Etag)
+	w.Header().Set("ETag", fmt.Sprintf("%q", obj.Etag))
 	for name, value := range obj.Metadata {
 		w.Header().Set("X-Goog-Meta-"+name, value)
 	}
diff --git a/fakestorage/upload.go b/fakestorage/upload.go
index 3aaeb0f..c85de72 100644
--- a/fakestorage/upload.go
+++ b/fakestorage/upload.go
@@ -504,7 +504,7 @@ func (s *Server) uploadFileContent(r *http.Request) jsonResponse {
 	obj.Content = append(obj.Content, content...)
 	obj.Crc32c = checksum.EncodedCrc32cChecksum(obj.Content)
 	obj.Md5Hash = checksum.EncodedMd5Hash(obj.Content)
-	obj.Etag = fmt.Sprintf("%q", obj.Md5Hash)
+	obj.Etag = obj.Md5Hash
 	contentTypeHeader := r.Header.Get(contentTypeHeader)
 	if contentTypeHeader != "" {
 		obj.ContentType = contentTypeHeader
diff --git a/go.mod b/go.mod
index 61d2d8b..4220b76 100644
--- a/go.mod
+++ b/go.mod
@@ -2,17 +2,17 @@ module github.com/fsouza/fake-gcs-server
 
 require (
 	cloud.google.com/go/pubsub v1.36.2
-	cloud.google.com/go/storage v1.39.0
+	cloud.google.com/go/storage v1.39.1
 	github.com/fsouza/slognil v0.4.0
 	github.com/google/go-cmp v0.6.0
 	github.com/gorilla/handlers v1.5.2
 	github.com/gorilla/mux v1.8.1
-	github.com/minio/minio-go/v7 v7.0.68
+	github.com/minio/minio-go/v7 v7.0.69
 	github.com/pkg/xattr v0.4.9
 	github.com/stretchr/testify v1.9.0
 	golang.org/x/oauth2 v0.18.0
 	google.golang.org/api v0.168.0
-	google.golang.org/genproto/googleapis/api v0.0.0-20240221002015-b0ce06bbee7c
+	google.golang.org/genproto/googleapis/api v0.0.0-20240304161311-37d4d3c04a78
 )
 
 require (
@@ -41,7 +41,7 @@ require (
 )
 
 require (
-	cloud.google.com/go v0.112.0 // indirect
+	cloud.google.com/go v0.112.1 // indirect
 	cloud.google.com/go/compute v1.24.0 // indirect
 	cloud.google.com/go/iam v1.1.6
 	github.com/davecgh/go-spew v1.1.1 // indirect
diff --git a/go.sum b/go.sum
index 43f51f4..47ba43e 100644
--- a/go.sum
+++ b/go.sum
@@ -1,6 +1,6 @@
 cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
-cloud.google.com/go v0.112.0 h1:tpFCD7hpHFlQ8yPwT3x+QeXqc2T6+n6T+hmABHfDUSM=
-cloud.google.com/go v0.112.0/go.mod h1:3jEEVwZ/MHU4djK5t5RHuKOA/GbLddgTdVubX1qnPD4=
+cloud.google.com/go v0.112.1 h1:uJSeirPke5UNZHIb4SxfZklVSiWWVqW4oXlETwZziwM=
+cloud.google.com/go v0.112.1/go.mod h1:+Vbu+Y1UU+I1rjmzeMOb/8RfkKJK2Gyxi1X6jJCZLo4=
 cloud.google.com/go/compute v1.24.0 h1:phWcR2eWzRJaL/kOiJwfFsPs4BaKq1j6vnpZrc1YlVg=
 cloud.google.com/go/compute v1.24.0/go.mod h1:kw1/T+h/+tK2LJK0wiPPx1intgdAM3j/g3hFDlscY40=
 cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
@@ -11,8 +11,8 @@ cloud.google.com/go/kms v1.15.7 h1:7caV9K3yIxvlQPAcaFffhlT7d1qpxjB1wHBtjWa13SM=
 cloud.google.com/go/kms v1.15.7/go.mod h1:ub54lbsa6tDkUwnu4W7Yt1aAIFLnspgh0kPGToDukeI=
 cloud.google.com/go/pubsub v1.36.2 h1:nAUD4aiWHZFYyINhRag1qOnHUk0/7QiWEa04XWnqACA=
 cloud.google.com/go/pubsub v1.36.2/go.mod h1:mHCFLNG8abCrPzhuOnpBcr9DUy+l3/LWWn0qoJdbh1w=
-cloud.google.com/go/storage v1.39.0 h1:brbjUa4hbDHhpQf48tjqMaXEV+f1OGoaTmQau9tmCsA=
-cloud.google.com/go/storage v1.39.0/go.mod h1:OAEj/WZwUYjA3YHQ10/YcN9ttGuEpLwvaoyBXIPikEk=
+cloud.google.com/go/storage v1.39.1 h1:MvraqHKhogCOTXTlct/9C3K3+Uy2jBmFYb3/Sp6dVtY=
+cloud.google.com/go/storage v1.39.1/go.mod h1:xK6xZmxZmo+fyP7+DEF6FhNc24/JAe95OLyOHCXFH1o=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
@@ -89,8 +89,8 @@ github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/4
 github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
 github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
 github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
-github.com/minio/minio-go/v7 v7.0.68 h1:hTqSIfLlpXaKuNy4baAp4Jjy2sqZEN9hRxD0M4aOfrQ=
-github.com/minio/minio-go/v7 v7.0.68/go.mod h1:XAvOPJQ5Xlzk5o3o/ArO2NMbhSGkimC+bpW/ngRKDmQ=
+github.com/minio/minio-go/v7 v7.0.69 h1:l8AnsQFyY1xiwa/DaQskY4NXSLA2yrGsW5iD9nRPVS0=
+github.com/minio/minio-go/v7 v7.0.69/go.mod h1:XAvOPJQ5Xlzk5o3o/ArO2NMbhSGkimC+bpW/ngRKDmQ=
 github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
 github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=
 github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -127,8 +127,8 @@ go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
 go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
 go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI=
 go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=
-go.opentelemetry.io/otel/sdk v1.21.0 h1:FTt8qirL1EysG6sTQRZ5TokkU8d0ugCj8htOgThZXQ8=
-go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E=
+go.opentelemetry.io/otel/sdk v1.22.0 h1:6coWHw9xw7EfClIC/+O31R8IY3/+EiRFHevmHafB2Gw=
+go.opentelemetry.io/otel/sdk v1.22.0/go.mod h1:iu7luyVGYovrRpe2fmj3CVKouQNdTOkxtLzPvPz1DOc=
 go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
 go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@@ -205,8 +205,8 @@ google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98
 google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
 google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 h1:9+tzLLstTlPTRyJTh+ah5wIMsBW5c4tQwGTN3thOW9Y=
 google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9/go.mod h1:mqHbVIp48Muh7Ywss/AD6I5kNVKZMmAa/QEW58Gxp2s=
-google.golang.org/genproto/googleapis/api v0.0.0-20240221002015-b0ce06bbee7c h1:9g7erC9qu44ks7UK4gDNlnk4kOxZG707xKm4jVniy6o=
-google.golang.org/genproto/googleapis/api v0.0.0-20240221002015-b0ce06bbee7c/go.mod h1:5iCWqnniDlqZHrd3neWVTOwvh/v6s3232omMecelax8=
+google.golang.org/genproto/googleapis/api v0.0.0-20240304161311-37d4d3c04a78 h1:SzXBGiWM1LNVYLCRP3e0/Gsze804l4jGoJ5lYysEO5I=
+google.golang.org/genproto/googleapis/api v0.0.0-20240304161311-37d4d3c04a78/go.mod h1:O1cOfN1Cy6QEYr7VxtjOyP5AdAuR0aJ/MYZaaof623Y=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20240304161311-37d4d3c04a78 h1:Xs9lu+tLXxLIfuci70nG4cpwaRC+mRQPUL7LoIeDJC4=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20240304161311-37d4d3c04a78/go.mod h1:UCOku4NytXMJuLQE5VuqA5lX3PcHCBo8pxNyvkf4xBs=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
diff --git a/internal/backend/fs.go b/internal/backend/fs.go
index 64f1106..c96867d 100644
--- a/internal/backend/fs.go
+++ b/internal/backend/fs.go
@@ -251,7 +251,7 @@ func (s *storageFS) CreateObject(obj StreamingObject, conditions Conditions) (St
 		obj.Md5Hash = hasher.EncodedMd5Hash()
 	}
 	if obj.Etag == "" {
-		obj.Etag = fmt.Sprintf("%q", obj.Md5Hash)
+		obj.Etag = obj.Md5Hash
 	}
 
 	// TODO: Handle if metadata is not present more gracefully?
diff --git a/internal/backend/memory.go b/internal/backend/memory.go
index 5075bf1..c32f06a 100644
--- a/internal/backend/memory.go
+++ b/internal/backend/memory.go
@@ -44,7 +44,7 @@ func (bm *bucketInMemory) addObject(obj Object) Object {
 		obj.Md5Hash = checksum.EncodedMd5Hash(obj.Content)
 	}
 	if obj.Etag == "" {
-		obj.Etag = fmt.Sprintf("%q", obj.Md5Hash)
+		obj.Etag = obj.Md5Hash
 	}
 	if obj.Size == 0 {
 		obj.Size = int64(len(obj.Content))


Test patch:
diff --git a/.github/workflows/docker-tests.yml b/.github/workflows/docker-tests.yml
index c9de154..6d8818a 100644
--- a/.github/workflows/docker-tests.yml
+++ b/.github/workflows/docker-tests.yml
@@ -9,7 +9,7 @@ jobs:
     name: test-dockerfile
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v4.1.1
+      - uses: actions/checkout@v4.1.2
 
       - name: setup qemu
         uses: docker/setup-qemu-action@v3.0.0
@@ -19,7 +19,7 @@ jobs:
         uses: docker/setup-buildx-action@v3.1.0
 
       - name: build
-        uses: docker/build-push-action@v5.1.0
+        uses: docker/build-push-action@v5.2.0
         with:
           cache-from: fsouzaci/gha-cache:fake-gcs-server
           context: .
diff --git a/.github/workflows/goreleaser-test.yml b/.github/workflows/goreleaser-test.yml
index add66d5..10082ca 100644
--- a/.github/workflows/goreleaser-test.yml
+++ b/.github/workflows/goreleaser-test.yml
@@ -14,7 +14,7 @@ jobs:
   test-goreleaser:
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v4.1.1
+      - uses: actions/checkout@v4.1.2
 
       - uses: actions/setup-go@v5.0.0
         with:
diff --git a/fakestorage/object_test.go b/fakestorage/object_test.go
index b158158..a943bf0 100644
--- a/fakestorage/object_test.go
+++ b/fakestorage/object_test.go
@@ -177,7 +177,7 @@ func checkObjectAttrs(testObj Object, attrs *storage.ObjectAttrs, t *testing.T)
 	if testObj.Content != nil && !bytes.Equal(attrs.MD5, checksum.MD5Hash(testObj.Content)) {
 		t.Errorf("wrong hash returned\nwant %d\ngot   %d", checksum.MD5Hash(testObj.Content), attrs.MD5)
 	}
-	expectedEtag := fmt.Sprintf("\"%s\"", checksum.EncodedHash(attrs.MD5))
+	expectedEtag := checksum.EncodedHash(attrs.MD5)
 	if attrs.Etag != expectedEtag {
 		t.Errorf("wrong Etag returned\nwant %s\ngot   %s", expectedEtag, attrs.Etag)
 	}
@@ -2028,7 +2028,7 @@ func TestServiceClientComposeObject(t *testing.T) {
 				"files/destination.txt",
 				[]string{"01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "30", "31", "32", "33"},
 				"",
-				"googleapi: Error 400: The number of source components provided (33) exceeds the maximum (32)",
+				"googleapi: Error 400: The number of source components provided (33) exceeds the maximum (32), Bad Request",
 			},
 		}
 		for _, test := range tests {
diff --git a/fakestorage/upload_test.go b/fakestorage/upload_test.go
index 79908fa..582c913 100644
--- a/fakestorage/upload_test.go
+++ b/fakestorage/upload_test.go
@@ -224,7 +224,7 @@ func TestServerClientObjectWriterWithDoesNotExistPrecondition(t *testing.T) {
 		if err == nil {
 			t.Fatal("expected overwriting existing object to fail, but received no error")
 		}
-		if err.Error() != "googleapi: Error 412: Precondition failed" {
+		if err.Error() != "googleapi: Error 412: Precondition failed, Precondition Failed" {
 			t.Errorf("expected HTTP 412 precondition failed error, but got %v", err)
 		}
 
@@ -346,7 +346,7 @@ func TestServerClientObjectOperationsWithIfGenerationNotMatchPrecondition(t *tes
 		if err == nil {
 			t.Fatal("expected overwriting existing object to fail, but received no error")
 		}
-		if err.Error() != "googleapi: Error 412: Precondition failed" {
+		if err.Error() != "googleapi: Error 412: Precondition failed, Precondition Failed" {
 			t.Errorf("expected HTTP 412 precondition failed error, but got %v", err)
 		}
 
diff --git a/internal/backend/backend_test.go b/internal/backend/backend_test.go
index 80f2ed8..949b81a 100644
--- a/internal/backend/backend_test.go
+++ b/internal/backend/backend_test.go
@@ -294,7 +294,6 @@ func TestBucketAttrsStoreRetrieveUpdate(t *testing.T) {
 }
 
 func TestBucketCreateGetListDelete(t *testing.T) {
-	startTime := time.Now()
 	testForStorageBackends(t, func(t *testing.T, storage Storage) {
 		buckets, err := storage.ListBuckets()
 		if err != nil {
@@ -312,12 +311,9 @@ func TestBucketCreateGetListDelete(t *testing.T) {
 			if err == nil {
 				t.Fatalf("bucket %s, exists before being created", bucket.Name)
 			}
-			// The FS backend uses filesystem timestamps to store bucket creation time.
-			// Use a large +/- 5 second window to allow for an imperfectly synchronized
-			// clock generating the filesystem timestamp and to reduce test flakes.
-			timeBeforeCreation := time.Now().Add(-5 * time.Second)
+			timeBeforeCreation := time.Now()
 			err = storage.CreateBucket(bucket.Name, BucketAttrs{VersioningEnabled: bucket.VersioningEnabled})
-			timeAfterCreation := time.Now().Add(5 * time.Second)
+			timeAfterCreation := time.Now()
 			if reflect.TypeOf(storage) == reflect.TypeOf(&storageFS{}) && bucket.VersioningEnabled {
 				if err == nil {
 					t.Fatal("fs storage should not accept creating buckets with versioning, but it's not failing")
@@ -332,7 +328,7 @@ func TestBucketCreateGetListDelete(t *testing.T) {
 				t.Fatal(err)
 			}
 			if !isBucketEquivalentTo(bucketFromStorage, bucket, timeBeforeCreation, timeAfterCreation) {
-				t.Errorf("bucket %v does not have the expected props after retrieving. Expected %v and time between %v and %v",
+				t.Errorf("bucket %v does not have the expected props after retrieving. Expected %v and time roughly between %v and %v",
 					bucketFromStorage, bucket, timeBeforeCreation, timeAfterCreation)
 			}
 			buckets, err = storage.ListBuckets()
@@ -345,8 +341,8 @@ func TestBucketCreateGetListDelete(t *testing.T) {
 			if buckets[0].Name != bucket.Name {
 				t.Errorf("listed bucket has unexpected name. Expected %s, actual: %v", bucket.Name, buckets[0].Name)
 			}
-			if buckets[0].TimeCreated.Before(startTime.Truncate(time.Second)) || time.Now().Before(buckets[0].TimeCreated) {
-				t.Errorf("listed bucket has unexpected creation time. Expected between test start time %v and now %v, actual: %v", startTime, time.Now(), buckets[0].TimeCreated)
+			if !isTimeRoughlyInRange(buckets[0].TimeCreated, timeBeforeCreation, timeAfterCreation) {
+				t.Errorf("listed bucket has unexpected creation time. Expected roughly between %v and %v, actual: %v", timeBeforeCreation, timeAfterCreation, buckets[0].TimeCreated)
 			}
 			err = storage.DeleteBucket(bucket.Name)
 			if err != nil {
@@ -356,10 +352,19 @@ func TestBucketCreateGetListDelete(t *testing.T) {
 	})
 }
 
-func isBucketEquivalentTo(a, b Bucket, earliest, latest time.Time) bool {
+func isBucketEquivalentTo(a, b Bucket, before, after time.Time) bool {
 	return a.Name == b.Name &&
 		a.VersioningEnabled == b.VersioningEnabled &&
-		a.TimeCreated.After(earliest) && a.TimeCreated.Before(latest)
+		isTimeRoughlyInRange(a.TimeCreated, before, after)
+}
+
+func isTimeRoughlyInRange(t, before, after time.Time) bool {
+	// The FS backend uses filesystem timestamps to store bucket creation time.
+	// Use a large +/- 5 second window to allow for an imperfectly synchronized
+	// clock generating the filesystem timestamp and to reduce test flakes.
+	earliest := before.Add(-5 * time.Second)
+	latest := after.Add(5 * time.Second)
+	return t.After(earliest) && t.Before(latest)
 }
 
 func TestBucketDuplication(t *testing.T) {