Files
seaweedfs/weed/server/filer_server_tus_transient_test.go
T
Chris Lu cda43f1976 filer: do not 404 a TUS session on a transient chunk-load failure (#11153)
* filer: do not 404 a TUS session on a transient chunk-load failure

readTusSessionInfo already proved the session exists before
loadTusSessionChunks is called, so a failure there is a read failure,
not evidence the session is gone: a volume-server timeout or a
canceled request context surfaces through ListDirectoryEntries the
same way a missing session would.

Every such error was mapped to writeTusSessionNotFound, answering 404
to HEAD/PATCH and 204 to DELETE. A spec-compliant TUS client trusts
that and discards the session, orphaning every chunk it had committed
until the 24h expiry sweep, or forever if it never issues a DELETE.

Only an error matching filer_pb.ErrNotFound is now reported as not
found; anything else answers 500 so the client retries against the
same session instead of abandoning it.

* test: cover a TUS session's transient chunk-load failure

Adds a listErr hook to the in-memory test store, alongside the
existing commitErr/deleteErr, to simulate a store or RPC failure from
ListDirectoryEntries.

HEAD, PATCH and DELETE against a live session all answer with a
server error instead of a not-found status when the chunk listing
fails transiently, and the session is left on disk untouched. A
listing failure that genuinely means not found, filer_pb.ErrNotFound,
still answers 404 (204 for DELETE).
2026-09-04 16:39:24 -07:00

87 lines
3.2 KiB
Go

package weed_server
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
"github.com/seaweedfs/seaweedfs/weed/util"
)
// TestFilerServer_tusHandler_TransientChunkLoadFailure reproduces #11151: a
// transient failure listing a session's chunk directory (volume timeout,
// canceled context) must not be reported as 404/204, since readTusSessionInfo
// already proved the session exists. A false "not found" makes a compliant
// client discard a live session and orphan every chunk it committed.
func TestFilerServer_tusHandler_TransientChunkLoadFailure(t *testing.T) {
tests := []struct {
name string
method string
}{
{"HEAD", http.MethodHead},
{"PATCH", http.MethodPatch},
{"DELETE", http.MethodDelete},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fs, store := newTusTestServer(t, map[string]string{tusTestUploadID: "/buckets/data/file.bin"})
store.listErr = context.Canceled
headers := map[string]string{"Authorization": "Bearer " + signFilerToken(t, tusTestWriteKey, nil, nil)}
if tt.method == http.MethodHead {
headers["Authorization"] = "Bearer " + signFilerToken(t, tusTestReadKey, nil, nil)
}
if tt.method == http.MethodPatch {
headers["Content-Type"] = "application/offset+octet-stream"
headers["Upload-Offset"] = "0"
}
req := tusRequest(tt.method, "/.tus/.uploads/"+tusTestUploadID, headers, "")
rec := httptest.NewRecorder()
fs.tusHandler(rec, req)
if rec.Code == http.StatusNotFound || rec.Code == http.StatusNoContent {
t.Fatalf("%s on a transient chunk-load failure = %d, want a server error, not a not-found status", tt.method, rec.Code)
}
if rec.Code < 500 {
t.Errorf("%s on a transient chunk-load failure = %d, want a 5xx status", tt.method, rec.Code)
}
store.listErr = nil
if _, err := store.FindEntry(context.Background(), util.FullPath(fs.tusSessionInfoPath(tusTestUploadID))); err != nil {
t.Errorf("session removed after a transient %s failure: %v", tt.method, err)
}
})
}
}
// TestFilerServer_tusHandler_ChunkLoadNotFoundStillNotFound verifies a chunk
// listing failure that genuinely means "not found" is still reported as such,
// so the fix for #11151 does not mask a real absence.
func TestFilerServer_tusHandler_ChunkLoadNotFoundStillNotFound(t *testing.T) {
fs, store := newTusTestServer(t, map[string]string{tusTestUploadID: "/buckets/data/file.bin"})
store.listErr = filer_pb.ErrNotFound
req := tusRequest(http.MethodHead, "/.tus/.uploads/"+tusTestUploadID, map[string]string{
"Authorization": "Bearer " + signFilerToken(t, tusTestReadKey, nil, nil),
}, "")
rec := httptest.NewRecorder()
fs.tusHandler(rec, req)
if rec.Code != http.StatusNotFound {
t.Fatalf("HEAD on a genuinely missing session = %d, want %d", rec.Code, http.StatusNotFound)
}
deleteReq := tusRequest(http.MethodDelete, "/.tus/.uploads/"+tusTestUploadID, map[string]string{
"Authorization": "Bearer " + signFilerToken(t, tusTestWriteKey, nil, nil),
}, "")
deleteRec := httptest.NewRecorder()
fs.tusHandler(deleteRec, deleteReq)
if deleteRec.Code != http.StatusNoContent {
t.Fatalf("DELETE on a genuinely missing session = %d, want %d", deleteRec.Code, http.StatusNoContent)
}
}