Repository: fsouza/fake-gcs-server
Pull request #1970 (2025-05-26 10:03:42)

fakeserver: honor Stop
Problem: calling `server.Close()` does not actually close anything — requests can flow as they normally would.

That's because the client always uses `*muxTransport`, which wires directly into the handlers avoiding all the TCP/HTTP dance.

In days long gone, `Server.transport` used to be HTTP, and starting/stopping the fakeserver did what one would expect.

Add a test that asserts correct behavior (we depend on it in some our tests), also removing the `Server.transport` ambiguity, as there is no other use case in this repo.

motiejus: Having made this PR, I am not entirely sure what's the role of the explicit listeners, given that the client is always created directly attached to `s.transport`, which points direclty to `*muxHandler`?

https://github.com/fsouza/fake-gcs-server/blob/79d71622f893b4661a435b34ca9a197a911a8d43/fakestorage/server.go#L493-L496

Patch:
diff --git a/.github/workflows/docker-push.yml b/.github/workflows/docker-push.yml
index 4ab628a..bee18cd 100644
--- a/.github/workflows/docker-push.yml
+++ b/.github/workflows/docker-push.yml
@@ -38,7 +38,7 @@ jobs:
           password: ${{ secrets.DOCKER_PASSWORD }}
 
       - name: build and push
-        uses: docker/build-push-action@v6.16.0
+        uses: docker/build-push-action@v6.18.0
         with:
           cache-from: type=gha
           cache-to: type=gha,mode=max
diff --git a/Dockerfile b/Dockerfile
index 73fc471..236f232 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -10,7 +10,7 @@ ADD . ./
 ENV CGO_ENABLED=0
 RUN go build -o fake-gcs-server
 
-FROM alpine:3.21.3
+FROM alpine:3.22.0
 RUN apk add --no-cache mailcap
 COPY --from=builder /code/fake-gcs-server /bin/fake-gcs-server
 RUN /bin/fake-gcs-server -h
diff --git a/fakestorage/mux_transport.go b/fakestorage/mux_transport.go
index b228c78..a3b9ca7 100644
--- a/fakestorage/mux_transport.go
+++ b/fakestorage/mux_transport.go
@@ -5,15 +5,20 @@
 package fakestorage
 
 import (
+	"errors"
 	"net/http"
 	"net/http/httptest"
 )
 
 type muxTransport struct {
 	handler http.Handler
+	closed  bool
 }
 
 func (t *muxTransport) RoundTrip(r *http.Request) (*http.Response, error) {
+	if t.closed {
+		return nil, errors.New("server closed")
+	}
 	w := httptest.NewRecorder()
 	t.handler.ServeHTTP(w, r)
 	return w.Result(), nil
diff --git a/fakestorage/server.go b/fakestorage/server.go
index e64e585..0f9d048 100644
--- a/fakestorage/server.go
+++ b/fakestorage/server.go
@@ -44,7 +44,7 @@ const defaultPublicHost = "storage.googleapis.com"
 type Server struct {
 	backend      backend.Storage
 	uploads      sync.Map
-	transport    http.RoundTripper
+	transport    *muxTransport
 	ts           *httptest.Server
 	handler      http.Handler
 	options      Options
@@ -455,10 +455,8 @@ func (s *Server) publicHostMatcher(r *http.Request, rm *mux.RouteMatch) bool {
 
 // Stop stops the server, closing all connections.
 func (s *Server) Stop() {
+	s.transport.closed = true
 	if s.ts != nil {
-		if transport, ok := s.transport.(*http.Transport); ok {
-			transport.CloseIdleConnections()
-		}
 		s.ts.Close()
 	}
 }
diff --git a/go.mod b/go.mod
index 460fea5..ff575d1 100644
--- a/go.mod
+++ b/go.mod
@@ -7,7 +7,7 @@ require (
 	github.com/google/go-cmp v0.7.0
 	github.com/gorilla/handlers v1.5.2
 	github.com/gorilla/mux v1.8.1
-	github.com/minio/minio-go/v7 v7.0.91
+	github.com/minio/minio-go/v7 v7.0.92
 	github.com/pkg/xattr v0.4.10
 	github.com/stretchr/testify v1.10.0
 	golang.org/x/oauth2 v0.30.0
@@ -37,8 +37,10 @@ require (
 	github.com/klauspost/cpuid/v2 v2.2.10 // indirect
 	github.com/minio/crc64nvme v1.0.1 // indirect
 	github.com/minio/md5-simd v1.1.2 // indirect
+	github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect
 	github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
 	github.com/rs/xid v1.6.0 // indirect
+	github.com/tinylib/msgp v1.3.0 // indirect
 	go.opentelemetry.io/auto/sdk v1.1.0 // indirect
 	go.opentelemetry.io/contrib/detectors/gcp v1.33.0 // indirect
 	go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0 // indirect
diff --git a/go.sum b/go.sum
index 7193e26..3acd28c 100644
--- a/go.sum
+++ b/go.sum
@@ -125,8 +125,10 @@ github.com/minio/crc64nvme v1.0.1 h1:DHQPrYPdqK7jQG/Ls5CTBZWeex/2FMS3G5XGkycuFrY
 github.com/minio/crc64nvme v1.0.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
 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.91 h1:tWLZnEfo3OZl5PoXQwcwTAPNNrjyWwOh6cbZitW5JQc=
-github.com/minio/minio-go/v7 v7.0.91/go.mod h1:uvMUcGrpgeSAAI6+sD3818508nUyMULw94j2Nxku/Go=
+github.com/minio/minio-go/v7 v7.0.92 h1:jpBFWyRS3p8P/9tsRc+NuvqoFi7qAmTCFPoRFmobbVw=
+github.com/minio/minio-go/v7 v7.0.92/go.mod h1:vTIc8DNcnAZIhyFsk8EB90AbPjj3j68aWIEQCiPj7d0=
+github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY=
+github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
 github.com/pkg/xattr v0.4.10 h1:Qe0mtiNFHQZ296vRgUjRCoPHPqH7VdTOrZx3g0T+pGA=
 github.com/pkg/xattr v0.4.10/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU=
 github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
@@ -146,6 +148,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
 github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
 github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
 github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/tinylib/msgp v1.3.0 h1:ULuf7GPooDaIlbyvgAxBV/FI7ynli6LZ1/nVUNu+0ww=
+github.com/tinylib/msgp v1.3.0/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0=
 go.einride.tech/aip v0.68.0 h1:4seM66oLzTpz50u4K1zlJyOXQ3tCzcJN7I22tKkjipw=
 go.einride.tech/aip v0.68.0/go.mod h1:7y9FF8VtPWqpxuAxl0KQWqaULxW4zFIesD6zF5RIHHg=
 go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=


Test patch:
diff --git a/.github/workflows/docker-tests.yml b/.github/workflows/docker-tests.yml
index 2de4d8b..92d995c 100644
--- a/.github/workflows/docker-tests.yml
+++ b/.github/workflows/docker-tests.yml
@@ -19,7 +19,7 @@ jobs:
         uses: docker/setup-buildx-action@v3.10.0
 
       - name: build
-        uses: docker/build-push-action@v6.16.0
+        uses: docker/build-push-action@v6.18.0
         with:
           cache-from: type=gha
           cache-to: type=gha,mode=max
diff --git a/fakestorage/server_test.go b/fakestorage/server_test.go
index bbdbd07..4f6765a 100644
--- a/fakestorage/server_test.go
+++ b/fakestorage/server_test.go
@@ -544,7 +544,9 @@ func testDownloadObject(t *testing.T, server *Server) {
 	for _, test := range tests {
 		test := test
 		t.Run(test.name, func(t *testing.T) {
-			t.Parallel()
+			// The tests cannot be run in parallel, because if one of them
+			// fails early, the parent test will close the server, failing the
+			// remaining ones.
 			client := server.HTTPClient()
 			url := server.scheme() + test.url
 			req, err := http.NewRequest(test.method, url, nil)
@@ -1258,7 +1260,6 @@ func runServersTest(t *testing.T, runOpts runServersOptions, fn func(*testing.T,
 		)
 	}
 	for _, test := range testScenarios {
-		test := test
 		t.Run(test.name, func(t *testing.T) {
 			t.Parallel()
 			server, err := NewServerWithOptions(test.options)
@@ -1270,3 +1271,21 @@ func runServersTest(t *testing.T, runOpts runServersOptions, fn func(*testing.T,
 		})
 	}
 }
+
+func TestServerStartStop(t *testing.T) {
+	ctx := context.Background()
+
+	server, err := NewServerWithOptions(Options{InitialObjects: []Object{
+		{ObjectAttrs: ObjectAttrs{BucketName: "some-bucket", Name: "text-01.txt"}, Content: []byte("something")},
+	}})
+	assert.NoError(t, err)
+	client := server.Client()
+
+	_, err = client.Bucket("some-bucket").Object("text-01.txt").NewReader(ctx)
+	assert.NoError(t, err)
+
+	server.Stop()
+
+	_, err = client.Bucket("some-bucket").Object("text-01.txt").NewReader(ctx)
+	assert.EqualError(t, err, `Get "https://storage.googleapis.com/some-bucket/text-01.txt": server closed`)
+}