You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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))
}

关键修复说明

  1. 移除冲突配置:删除STORAGE_EMULATOR_HOST环境变量设置,仅通过option.WithEndpoint配置客户端端点
  2. 修正public-host参数:使用Testcontainers模板变量{{.Port}}自动替换为宿主机动态端口,确保模拟器生成的访问地址正确
  3. 资源管理优化:添加客户端、Writer、Reader的延迟关闭,避免资源泄漏
  4. 测试断言标准化:使用testify断言替代手动打印,让测试结果更清晰

内容的提问来源于stack exchange,提问作者Robert Trzebiński

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.13 16:05:29