googleapis / googleapis/gapic-showcase
FR: Add test scenario `non_fatal_error_on_finalize` for resumable uploads
- 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
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