From 922da29f679cb32852966bb888c02c3dd64749c9 Mon Sep 17 00:00:00 2001 From: shollands Date: Tue, 10 Mar 2026 14:38:37 -0700 Subject: [PATCH] Fix: return HTTP 409 instead of 500 for duplicate datasets and jobs When creating a dataset or job that already exists, the emulator returns HTTP 500 (InternalServerError) instead of HTTP 409 (Conflict). This causes the BigQuery Python client to retry with exponential backoff for up to 600 seconds, since it treats 500 as transient. The exists_ok=True parameter also fails to suppress the error because it only checks for 409. This fix follows the existing ErrDuplicatedTable pattern already in the codebase: sentinel errors in the metadata package, checked with errors.Is in ServeHTTP, mapped to errDuplicate() for the HTTP response. Handle() method signatures are unchanged, addressing the feedback on #184. Fixes #256 Supersedes #184 Co-Authored-By: Claude Opus 4.6 --- internal/metadata/project.go | 10 +++++-- server/handler.go | 12 +++++++-- server/server_test.go | 52 ++++++++++++++++++++++++++++++++++++ 3 files changed, 70 insertions(+), 4 deletions(-) diff --git a/internal/metadata/project.go b/internal/metadata/project.go index cecca2578..0213eba8a 100644 --- a/internal/metadata/project.go +++ b/internal/metadata/project.go @@ -3,10 +3,16 @@ package metadata import ( "context" "database/sql" + "errors" "fmt" "sync" ) +var ( + ErrDuplicatedDataset = errors.New("dataset is already created") + ErrDuplicatedJob = errors.New("job is already created") +) + type Project struct { ID string datasets []*Dataset @@ -69,7 +75,7 @@ func (p *Project) AddDataset(ctx context.Context, tx *sql.Tx, dataset *Dataset) p.mu.Lock() defer p.mu.Unlock() if _, exists := p.datasetMap[dataset.ID]; exists { - return fmt.Errorf("dataset %s is already created", dataset.ID) + return fmt.Errorf("dataset %s: %w", dataset.ID, ErrDuplicatedDataset) } if err := dataset.Insert(ctx, tx); err != nil { return err @@ -111,7 +117,7 @@ func (p *Project) AddJob(ctx context.Context, tx *sql.Tx, job *Job) error { p.mu.Lock() defer p.mu.Unlock() if _, exists := p.jobMap[job.ID]; exists { - return fmt.Errorf("job %s is already created", job.ID) + return fmt.Errorf("job %s: %w", job.ID, ErrDuplicatedJob) } if err := job.Insert(ctx, tx); err != nil { return err diff --git a/server/handler.go b/server/handler.go index 7a5d2c0b7..8e6f53272 100644 --- a/server/handler.go +++ b/server/handler.go @@ -681,7 +681,11 @@ func (h *datasetsInsertHandler) ServeHTTP(w http.ResponseWriter, r *http.Request dataset: &dataset, }) if err != nil { - errorResponse(ctx, w, errInternalError(err.Error())) + if errors.Is(err, metadata.ErrDuplicatedDataset) { + errorResponse(ctx, w, errDuplicate(err.Error())) + } else { + errorResponse(ctx, w, errInternalError(err.Error())) + } return } encodeResponse(ctx, w, res) @@ -1016,7 +1020,11 @@ func (h *jobsInsertHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { job: &job, }) if err != nil { - errorResponse(ctx, w, errJobInternalError(err.Error())) + if errors.Is(err, metadata.ErrDuplicatedJob) { + errorResponse(ctx, w, errDuplicate(err.Error())) + } else { + errorResponse(ctx, w, errJobInternalError(err.Error())) + } return } encodeResponse(ctx, w, res) diff --git a/server/server_test.go b/server/server_test.go index a27f49208..e1bb89443 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -867,6 +867,58 @@ func TestDuplicateTable(t *testing.T) { } } +func TestDuplicateDataset(t *testing.T) { + const ( + projectName = "test" + datasetName = "dataset1" + ) + + ctx := context.Background() + + bqServer, err := server.New(server.TempStorage) + if err != nil { + t.Fatal(err) + } + if err := bqServer.SetProject(projectName); err != nil { + t.Fatal(err) + } + + testServer := bqServer.TestServer() + defer func() { + testServer.Close() + bqServer.Stop(ctx) + }() + + client, err := bigquery.NewClient( + ctx, + projectName, + option.WithEndpoint(testServer.URL), + option.WithoutAuthentication(), + ) + if err != nil { + t.Fatal(err) + } + defer client.Close() + + dataset := client.Dataset(datasetName) + if err := dataset.Create(ctx, &bigquery.DatasetMetadata{ + Name: datasetName, + }); err != nil { + t.Fatalf("%+v", err) + } + + if err := dataset.Create(ctx, &bigquery.DatasetMetadata{ + Name: datasetName, + }); err != nil { + ge := err.(*googleapi.Error) + if ge.Code != 409 { + t.Fatalf("expected 409 Conflict, got %d: %+v", ge.Code, ge) + } + } else { + t.Fatal("expected error when dataset name duplicates") + } +} + func TestDuplicateTableWithSchema(t *testing.T) { const ( projectName = "test"