googleapis / googleapis/gapic-showcase

FR: Add test scenario `non_fatal_error_on_finalize` for resumable uploads

Open
#1,682 3 comments 0 reactions 0 assignees View on GitHub
Dominant language
Go
Stars
183
Forks
55
Avg merge
1d 1h
Merged PRs (30d)
8

Description

Under the Cloud SDK Resumable Upload specification, transient server errors (such as `HTTP 500 Internal Server Error`, `HTTP 502 Bad Gateway`, or `HTTP 503 Service Unavailable`) can occur during the finalization phase (`finalize` command). When a recoverable error occurs during finalization, the server returns an HTTP 5xx error with `X-Goog-Upload-Status: active` and keeps the session active.

Client libraries are required to handle non-fatal finalize errors by querying upload status (`X-Goog-Upload-Command: query`) to verify that all bytes were committed, and then retrying the `finalize` command without re-uploading payload data.

Showcase currently supports injecting transient errors during session initiation (`non_fatal_error_on_start`), chunk uploads (`non_fatal_error_on_chunk_upload`), and queries (`non_fatal_error_on_query`), but lacks support for simulating non-fatal errors during `finalize`.

## Current Behavior

Showcase immediately finalizes sessions upon receiving the `finalize` command without checking for injected finalize failure scenarios.

See test scenarios below where `non_fatal_error_on_finalize` is missing:

https://github.com/googleapis/gapic-showcase/blob/dece585e7cb61ec0f28ffcc708b2843524be52b1/server/resumableupload/resumableupload.go#L64-L77

And finalize implementation:

https://github.com/googleapis/gapic-showcase/blob/dece585e7cb61ec0f28ffcc708b2843524be52b1/server/resumableupload/resumableupload.go#L261-L272

### Proposed Solution

Add the `non_fatal_error_on_finalize` test scenario:

• Trigger when `cmd == "finalize"`.
• While `UploadFailures < FailureCount`, increment `UploadFailures` and return an injected error code (default `HTTP 503 Service Unavailable` or configured `error_code`) with header `X-Goog-Upload-Status: active`.
• Keep the session status as `active` so subsequent `query` and retry `finalize` commands can be processed.
• If `action_after_failures == "terminate"`, terminate with `HTTP 500 Internal Server Error`.
• Allow subsequent `finalize` requests to succeed with `HTTP 200 OK` and `X-Goog-Upload-Status: final` once `UploadFailures >= FailureCount`.

### Minimal Go Test Case

```go
func TestNonFatalErrorOnFinalizeScenario(t *testing.T) {
mgr := resumableupload.NewManager()
handler := mgr.Middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
}))

// 1. Start session with non_fatal_error_on_finalize
reqStart := httptest.NewRequest("POST", "http://localhost:7469/upload", strings.NewReader(`{"name":"test.txt"}`))
reqStart.Header.Set("X-Goog-Upload-Protocol", "resumable")
reqStart.Header.Set("X-Goog-Upload-Command", "start")
reqStart.Header.Set("Content-Type", "application/json")
reqStart.Header.Set("X-Goog-Test-Scenario", "non_fatal_error_on_finalize")
reqStart.Header.Set("X-Goog-Test-Scenario-Config", `{"error_code":503,"failure_count":1}`)
recStart := httptest.NewRecorder()
handler.ServeHTTP(recStart, reqStart)

uploadURL := recStart.Header().Get("X-Goog-Upload-URL")
if uploadURL == "" {
t.Fatalf("expected X-Goog-Upload-URL in response, got empty")
}

// 2. Upload chunk (data payload)
reqChunk := httptest.NewRequest("POST", uploadURL, strings.NewReader("hello world"))
reqChunk.Header.Set("X-Goog-Upload-Command", "upload")
reqChunk.Header.Set("X-Goog-Upload-Offset", "0")
recChunk := httptest.NewRecorder()
handler.ServeHTTP(recChunk, reqChunk)

if recChunk.Code != http.StatusOK {
t.Fatalf("expected 200 OK on chunk upload, got %d", recChunk.Code)
}

// 3. First finalize attempt fails with injected 503 and status active
reqFinalizeFailed := httptest.NewRequest("POST", uploadURL, nil)
reqFinalizeFailed.Header.Set("X-Goog-Upload-Command", "finalize")
recFinalizeFailed := httptest.NewRecorder()
handler.ServeHTTP(recFinalizeFailed, reqFinalizeFailed)

if recFinalizeFailed.Code != http.StatusServiceUnavailable {
t.Fatalf("expected 503 Service Unavailable on first finalize attempt, got %d", recFinalizeFailed.Code)
}
if got := recFinalizeFailed.Header().Get("X-Goog-Upload-Status"); got != "active" {
t.Fatalf("expected X-Goog-Upload-Status active on non-fatal finalize error, got %q", got)
}

// 4. Query to verify committed offset is preserved
reqQuery := httptest.NewRequest("POST", uploadURL, nil)
reqQuery.Header.Set("X-Goog-Upload-Command", "query")
recQuery := httptest.NewRecorder()
handler.ServeHTTP(recQuery, reqQuery)

if recQuery.Code != http.StatusOK {
t.Fatalf("expected 200 OK on query, got %d", recQuery.Code)
}
if got := recQuery.Header().Get("X-Goog-Upload-Size-Received"); got != "11" {
t.Fatalf("expected query to report offset 11, got %q", got)
}

// 5. Finalize retry succeeds after exhausting failure_count=1
reqFinalizeRetry := httptest.NewRequest("POST", uploadURL, nil)
reqFinalizeRetry.Header.Set("X-Goog-Upload-Command", "finalize")
recFinalizeRetry := httptest.NewRecorder()
handler.ServeHTTP(recFinalizeRetry, reqFinalizeRetry)

if recFinalizeRetry.Code != http.StatusOK {
t.Fatalf("expected 200 OK on finalize retry, got %d", recFinalizeRetry.Code)
}
if got := recFinalizeRetry.Header().Get("X-Goog-Upload-Status"); got != "final" {
t.Fatalf("expected X-Goog-Upload-Status final on successful finalize, got %q", got)
}
expectedBody := `{"name":"test.txt","size":11}`
if got := strings.TrimSpace(recFinalizeRetry.Body.String()); got != expectedBody {
t.Fatalf("expected body %s, got %s", expectedBody, got)
}
}
```

```
partheniou@partheniou-vm-3:~/git/gapic-showcase$ go test -count=1 -v ./server/resumableupload -run TestNonFatalErrorOnFinalizeScenario
=== RUN TestNonFatalErrorOnFinalizeScenario
resumableupload_test.go:677: expected 503 Service Unavailable on first finalize attempt, got 200
--- FAIL: TestNonFatalErrorOnFinalizeScenario (0.00s)
FAIL
FAIL github.com/googleapis/gapic-showcase/server/resumableupload 0.012s
FAIL
```

Contributor guide

Open the contributing guide

Research direction

Start with server/resumableupload/resumableupload.go, especially the existing scenario definitions and finalize implementation at the linked lines. Run the provided go test command, then use the minimal test case in resumableupload_test.go to verify injected finalize failure, active status, preserved query offset, and successful retry with final status.

Written by the indexing model from the issue text.

Assessment

Tech stack
go
Domain
backend, testing
Issue type
Feature
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
78/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.