Go+Testcontainers+fake-gcs-server上传文件后无法访问问题
Go集成测试中fsouza/fake-gcs-server访问问题解决方案
问题描述
在Go集成测试中使用Testcontainers部署fsouza/fake-gcs-server模拟器时,文件上传正常(容器内可查看到文件),但无法通过代码或HTTP请求访问验证上传结果:
- 访问
http://localhost:{port}/storage/v1/foo/bar返回404 - 容器日志显示上传请求成功,但读取对象的GET请求均返回404
复现代码
package fsouza import ( "bytes" "cloud.google.com/go/storage" "context" "fmt" "github.com/docker/go-connections/nat" "github.com/stretchr/testify/assert" "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/wait" "google.golang.org/api/option" "io" "log" "os" "testing" "time" ) func TestUpload(t *testing.T) { ctx := context.Background() var port = "4443/tcp" req := testcontainers.GenericContainerRequest{ ContainerRequest: testcontainers.ContainerRequest{ Image: "fsouza/fake-gcs-server", ExposedPorts: []string{port}, Cmd: []string{"-scheme", "http", "-port", "4443", "-public-host", "localhost:4443"}, WaitingFor: wait.ForAll( wait.ForLog("server started at"), ), }, Started: true, } container, err := testcontainers.GenericContainer(ctx, req) defer container.Terminate(ctx) if err != nil { log.Fatal(err) } assert.True(t, container.IsRunning(), "container is not running") mappedPort, err := container.MappedPort(ctx, nat.Port(port)) if err != nil { log.Fatal(err) } log.Println("gcs container ready and running at port: ", mappedPort.Port()) // Set STORAGE_EMULATOR_HOST environment variable. err = os.Setenv("STORAGE_EMULATOR_HOST", fmt.Sprintf("%s:%s", "http://localhost", mappedPort.Port())) if err != nil { log.Fatalf("os.Setenv: %v", err) } endpoint := fmt.Sprintf("http://localhost:%s/storage/v1/", mappedPort.Port()) log.Println("Endpoint:", endpoint) s, err := storage.NewClient(ctx, option.WithEndpoint(endpoint)) if err != nil { t.Error(err) } // file to be uploaded var buffer bytes.Buffer buffer.WriteString("foo bar") bucketName := "foo" objectName := "bar" bucket := s.Bucket(bucketName) if err := bucket.Create(ctx, "abc", nil); err != nil { log.Printf("Failed to create bucket: %v", err) } o := s.Bucket(bucketName).Object(objectName) wc := o.NewWriter(ctx) written, err := io.Copy(wc, &buffer) if err != nil { log.Printf("failed to upload file, %v", err) } fmt.Printf("uploader - written: %v", written) err = wc.Close() if err != nil { t.Error(err) } log.Println("Creating reader") rd, err := bucket.Object(objectName).NewReader(ctx) if err != nil { log.Printf("failed to create reader, %v", err) } println("sleeping..") time.Sleep(1000 * time.Second) res, err := io.ReadAll(rd) if err != nil { t.Errorf("failed to read, %v", err) } fmt.Print(res) }
容器检查结果
$ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES ca2561b006f1 fsouza/fake-gcs-server "/bin/fake-gcs-serve…" 24 seconds ago Up 22 seconds 0.0.0.0:49407->4443/tcp, :::49402->4443/tcp eloquent_cohen e495202a08a7 testcontainers/ryuk:0.3.4 "/app" 27 seconds ago Up 25 seconds 0.0.0.0:49406->8080/tcp, :::49401->8080/tcp condescending_gould $ docker exec -it ca2561b006f1 sh / # ls /storage/foo/bar /storage/foo/bar / # cat /storage/foo/bar foo bar/ # exit $ docker logs ca2561b006f1 time="2022-11-06T13:51:13Z" level=info msg="couldn't load any objects or buckets from \"/data\", starting empty" time="2022-11-06T13:51:13Z" level=info msg="server started at http://[::]:4443" time="2022-11-06T13:51:16Z" level=info msg="172.17.0.1 - - [06/Nov/2022:13:51:16 +0000] \"GET /storage/v1/ HTTP/1.1\" 404 10" time="2022-11-06T13:51:16Z" level=info msg="172.17.0.1 - - [06/Nov/2022:13:51:16 +0000] \"GET /favicon.ico HTTP/1.1\" 404 10" time="2022-11-06T13:51:19Z" level=info msg="172.17.0.1 - - [06/Nov/2022:13:51:19 +0000] \"POST /storage/v1/b?alt=json&prettyPrint=false&project=abc HTTP/1.1\" 200 131" time="2022-11-06T13:51:19Z" level=info msg="172.17.0.1 - - [06/Nov/2022:13:51:19 +0000] \"POST /upload/storage/v1/b/foo/o?alt=json&name=bar&prettyPrint=false&projection=full&uploadType=multipart HTTP/1.1\" 200 442" time="2022-11-06T13:51:19Z" level=info msg="172.17.0.1 - - [06/Nov/2022:13:51:19 +0000] \"GET /foo/bar HTTP/1.1\" 404 10" time="2022-11-06T13:51:20Z" level=info msg="172.17.0.1 - - [06/Nov/2022:13:51:20 +0000] \"GET /storage/v1/foo/bar HTTP/1.1\" 404 10"
错误点分析
- 配置冲突:同时设置
STORAGE_EMULATOR_HOST环境变量和option.WithEndpoint,导致GCS客户端行为异常 - public-host参数错误:容器启动时指定的
-public-host为固定端口localhost:4443,但实际映射到宿主机的是动态端口,导致模拟器生成的对象访问地址不匹配 - API路径错误:手动拼接的
/storage/v1/foo/bar不符合GCS API规范,正确路径应为/storage/v1/b/{bucket}/o/{object}
修复方案
修改后的代码
package fsouza import ( "bytes" "cloud.google.com/go/storage" "context" "fmt" "github.com/docker/go-connections/nat" "github.com/stretchr/testify/assert" "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/wait" "google.golang.org/api/option" "io" "log" "testing" ) func TestUpload(t *testing.T) { ctx := context.Background() var port = "4443/tcp" req := testcontainers.GenericContainerRequest{ ContainerRequest: testcontainers.ContainerRequest{ Image: "fsouza/fake-gcs-server", ExposedPorts: []string{port}, // 使用Testcontainers模板变量自动替换为动态映射端口 Cmd: []string{"-scheme", "http", "-port", "4443", "-public-host", fmt.Sprintf("localhost:%s", "{{.Port}}")}, WaitingFor: wait.ForAll( wait.ForLog("server started at"), ), }, Started: true, } container, err := testcontainers.GenericContainer(ctx, req) defer container.Terminate(ctx) if err != nil { log.Fatal(err) } assert.True(t, container.IsRunning(), "container is not running") mappedPort, err := container.MappedPort(ctx, nat.Port(port)) if err != nil { log.Fatal(err) } log.Println("gcs container ready and running at port: ", mappedPort.Port()) // 移除冲突的环境变量,仅通过WithEndpoint配置客户端 endpoint := fmt.Sprintf("http://localhost:%s/storage/v1/", mappedPort.Port()) log.Println("Endpoint:", endpoint) s, err := storage.NewClient(ctx, option.WithEndpoint(endpoint)) if err != nil { t.Fatal(err) } defer s.Close() // 确保客户端资源释放 // 准备上传内容 var buffer bytes.Buffer buffer.WriteString("foo bar") bucketName := "foo" objectName := "bar" // 创建桶 bucket := s.Bucket(bucketName) if err := bucket.Create(ctx, "abc", nil); err != nil { log.Printf("Failed to create bucket: %v", err) } // 上传对象 o := s.Bucket(bucketName).Object(objectName) wc := o.NewWriter(ctx) defer wc.Close() written, err := io.Copy(wc, &buffer) if err != nil { t.Fatalf("failed to upload file: %v", err) } fmt.Printf("uploader - written: %v\n", written) // 关闭writer完成上传 if err := wc.Close(); err != nil { t.Fatal(err) } // 读取对象验证结果 log.Println("Creating reader") rd, err := bucket.Object(objectName).NewReader(ctx) if err != nil { t.Fatalf("failed to create reader: %v", err) } defer rd.Close() res, err := io.ReadAll(rd) if err != nil { t.Fatalf("failed to read: %v", err) } // 使用断言验证内容一致性 assert.Equal(t, "foo bar", string(res), "read content mismatch") fmt.Println("Read content:", string(res)) }
关键修复说明
- 移除冲突配置:删除
STORAGE_EMULATOR_HOST环境变量设置,仅通过option.WithEndpoint配置客户端端点 - 修正public-host参数:使用Testcontainers模板变量
{{.Port}}自动替换为宿主机动态端口,确保模拟器生成的访问地址正确 - 资源管理优化:添加客户端、Writer、Reader的延迟关闭,避免资源泄漏
- 测试断言标准化:使用
testify断言替代手动打印,让测试结果更清晰
内容的提问来源于stack exchange,提问作者Robert Trzebiński
相关产品推荐
相关产品推荐

