googleapis / googleapis/gapic-showcase
FR: Add test scenario `fatal_error_on_chunk_upload` 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, the server may permanently reject an upload during the data transmission phase (upload command) due to fatal errors such as `401 Unauthorized`, `403 Forbidden`, or `404 Not Found`. When a terminal error occurs, the server terminates the upload session with `X-Goog-Upload-Status: final` and returns an `HTTP 4xx` error (eg `HTTP 403 Forbidden`).
Client libraries are required to handle these terminal chunk upload errors by failing fast, terminating retry attempts immediately, and surfacing the error to the caller. Additionally, subsequent upload attempts to a finalized session must be rejected with `HTTP 400 Bad Request` and `X-Goog-Upload-Status: final`.
Showcase currently only supports injecting transient errors during chunk uploads (`non_fatal_error_on_chunk_upload`), preventing client libraries from validating fail-fast behavior and session termination mid-transfer against the showcase server.
## Current Behavior
Showcase supports terminal errors during session initiation (`fatal_error_on_start`), but chunk uploads only support recoverable errors (`non_fatal_error_on_chunk_upload`).
See test scenarios below where `fatal_error_on_chunk_upload` is missing
https://github.com/googleapis/gapic-showcase/blob/dece585e7cb61ec0f28ffcc708b2843524be52b1/server/resumableupload/resumableupload.go#L64-L77
### Proposed Solution
Add the `fatal_error_on_chunk_upload` test scenario:
• Trigger when `cmd == "upload"` and `offset >= after_offset`.
• Terminate the session by setting `sess.Status = statusFinal`.
• Return `HTTP 403 Forbidden` (or user configured error_code) with header `X-Goog-Upload-Status: final`.
• Ensure subsequent requests to the finalized session fail with `HTTP 400 Bad Request` and `X-Goog-Upload-Status: final`.
### Minimal Go Test Case
```
func TestFatalErrorOnChunkUploadScenario(t *testing.T) {
mgr := resumableupload.NewManager()
handler := mgr.Middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}))
// 1. Default fatal error on chunk upload (403 Forbidden, X-Goog-Upload-Status: final)
reqStartDefault := httptest.NewRequest("POST", "http://localhost:7469/upload", nil)
reqStartDefault.Header.Set("X-Goog-Upload-Protocol", "resumable")
reqStartDefault.Header.Set("X-Goog-Upload-Command", "start")
reqStartDefault.Header.Set("X-Goog-Test-Scenario", "fatal_error_on_chunk_upload")
recStartDefault := httptest.NewRecorder()
handler.ServeHTTP(recStartDefault, reqStartDefault)
defaultUploadURL := recStartDefault.Header().Get("X-Goog-Upload-URL")
if defaultUploadURL == "" {
t.Fatalf("expected X-Goog-Upload-URL in response, got empty")
}
reqChunkFatal := httptest.NewRequest("POST", defaultUploadURL, bytes.NewReader([]byte("chunk1")))
reqChunkFatal.Header.Set("X-Goog-Upload-Command", "upload")
reqChunkFatal.Header.Set("X-Goog-Upload-Offset", "0")
recChunkFatal := httptest.NewRecorder()
handler.ServeHTTP(recChunkFatal, reqChunkFatal)
if recChunkFatal.Code != http.StatusForbidden {
t.Fatalf("expected 403 Forbidden on fatal_error_on_chunk_upload default, got %d", recChunkFatal.Code)
}
if got := recChunkFatal.Header().Get("X-Goog-Upload-Status"); got != "final" {
t.Fatalf("expected X-Goog-Upload-Status final, got %q", got)
}
// Subsequent upload attempt should fail because the session is terminated (status is final)
reqChunkRetry := httptest.NewRequest("POST", defaultUploadURL, bytes.NewReader([]byte("chunk1")))
reqChunkRetry.Header.Set("X-Goog-Upload-Command", "upload")
reqChunkRetry.Header.Set("X-Goog-Upload-Offset", "0")
recChunkRetry := httptest.NewRecorder()
handler.ServeHTTP(recChunkRetry, reqChunkRetry)
if recChunkRetry.Code != http.StatusBadRequest {
t.Fatalf("expected 400 Bad Request on retry after fatal error, got %d", recChunkRetry.Code)
}
if got := recChunkRetry.Header().Get("X-Goog-Upload-Status"); got != "final" {
t.Fatalf("expected X-Goog-Upload-Status final on retry, got %q", got)
}
// 2. Custom fatal error on chunk upload with error_code: 400 and after_offset: 5
reqStartCustom := httptest.NewRequest("POST", "http://localhost:7469/upload", nil)
reqStartCustom.Header.Set("X-Goog-Upload-Protocol", "resumable")
reqStartCustom.Header.Set("X-Goog-Upload-Command", "start")
reqStartCustom.Header.Set("X-Goog-Test-Scenario", "fatal_error_on_chunk_upload")
reqStartCustom.Header.Set("X-Goog-Test-Scenario-Config", `{"error_code": 400, "after_offset": 5}`)
recStartCustom := httptest.NewRecorder()
handler.ServeHTTP(recStartCustom, reqStartCustom)
customUploadURL := recStartCustom.Header().Get("X-Goog-Upload-URL")
if customUploadURL == "" {
t.Fatalf("expected X-Goog-Upload-URL in response, got empty")
}
// First chunk at offset 0 (5 bytes) succeeds because after_offset is 5
reqChunkBeforeOffset := httptest.NewRequest("POST", customUploadURL, bytes.NewReader([]byte("12345")))
reqChunkBeforeOffset.Header.Set("X-Goog-Upload-Command", "upload")
reqChunkBeforeOffset.Header.Set("X-Goog-Upload-Offset", "0")
recChunkBeforeOffset := httptest.NewRecorder()
handler.ServeHTTP(recChunkBeforeOffset, reqChunkBeforeOffset)
if recChunkBeforeOffset.Code != http.StatusOK {
t.Fatalf("expected 200 OK for chunk before after_offset, got %d", recChunkBeforeOffset.Code)
}
// Second chunk at offset 5 triggers the fatal error (400 Bad Request, status final)
reqChunkAtOffset := httptest.NewRequest("POST", customUploadURL, bytes.NewReader([]byte("67890")))
reqChunkAtOffset.Header.Set("X-Goog-Upload-Command", "upload")
reqChunkAtOffset.Header.Set("X-Goog-Upload-Offset", "5")
recChunkAtOffset := httptest.NewRecorder()
handler.ServeHTTP(recChunkAtOffset, reqChunkAtOffset)
if recChunkAtOffset.Code != http.StatusBadRequest {
t.Fatalf("expected 400 Bad Request for chunk at after_offset, got %d", recChunkAtOffset.Code)
}
if got := recChunkAtOffset.Header().Get("X-Goog-Upload-Status"); got != "final" {
t.Fatalf("expected X-Goog-Upload-Status final, got %q", got)
}
}
```
```
partheniou@partheniou-vm-3:~/git/gapic-showcase$ go test -count=1 -v ./server/resumableupload -run TestFatalErrorOnChunkUploadScenario
=== RUN TestFatalErrorOnChunkUploadScenario
resumableupload_test.go:665: expected 403 Forbidden on fatal_error_on_chunk_upload default, got 200
--- FAIL: TestFatalErrorOnChunkUploadScenario (0.00s)
FAIL
FAIL github.com/googleapis/gapic-showcase/server/resumableupload 0.010s
FAIL
```
Contributor guide
Research direction
Start with server/resumableupload/resumableupload.go around the scenario handling at lines 64-77, then read the existing tests in server/resumableupload/resumableupload_test.go, including the failing test near line 665. Add coverage for default and configured fatal chunk-upload errors, including finalized-session behavior, and run go test -count=1 -v ./server/resumableupload -run TestFatalErrorOnChunkUploadScenario.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- api, backend
- Issue type
- Feature
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 78/100