fix: improve retry handling for expired HTTP redirect url (#1263)

This commit is contained in:
Levi authored and GitHub committed 2026-01-29 22:13:42 +08:00
1 parent fe90c1d261
commit 93088b22f1
5 files changed
+410 -8

No files matched your search

+162
View File
@@ -778,6 +778,168 @@ func TestFetcher_AsyncPrefetch(t *testing.T) {
})
}
// TestFetcher_DownloadExpiringRedirectURL tests that the fetcher correctly handles
// expiring redirect URLs by falling back to the original URL and getting a new redirect.
func TestFetcher_DownloadExpiringRedirectURL(t *testing.T) {
// Test with redirect expiring after 2 requests
// This means:
// - Request 1: Resolve (original URL redirects to temp URL v1)
// - Request 2: First download request to temp URL v1 succeeds
// - Request 3: Second download request to temp URL v1 returns 403
// - Fetcher should then retry with original URL, get temp URL v2
// - Continue downloading from temp URL v2
t.Run("RedirectExpiresAfter2Requests", func(t *testing.T) {
os.Remove(test.DownloadFile)
// Create server with redirect expiring after 2 requests, with slow transfer to ensure
// multiple connection attempts are needed
listener := test.StartTestExpiringRedirectServer(2, 100*time.Nanosecond)
defer listener.Close()
fetcher := buildFetcher()
err := fetcher.Resolve(&base.Request{
URL: "http://" + listener.Addr().String() + "/" + test.BuildName,
}, &base.Options{
Name: test.DownloadName,
Path: test.Dir,
Extra: &http.OptsExtra{
Connections: 4, // Use multiple connections to trigger redirect expiration
},
})
if err != nil {
t.Fatal(err)
}
err = fetcher.Start()
if err != nil {
t.Fatal(err)
}
err = fetcher.Wait()
if err != nil {
t.Fatal(err)
}
// Verify file content is correct despite redirect expiration
want := test.FileMd5(test.BuildFile)
got := test.FileMd5(test.DownloadFile)
if want != got {
t.Errorf("Download() got = %v, want %v", got, want)
}
os.Remove(test.DownloadFile)
})
// Test with redirect expiring after 5 requests (more room for initial connections)
t.Run("RedirectExpiresAfter5Requests", func(t *testing.T) {
os.Remove(test.DownloadFile)
listener := test.StartTestExpiringRedirectServer(5, 100*time.Nanosecond)
defer listener.Close()
fetcher := buildFetcher()
err := fetcher.Resolve(&base.Request{
URL: "http://" + listener.Addr().String() + "/" + test.BuildName,
}, &base.Options{
Name: test.DownloadName,
Path: test.Dir,
Extra: &http.OptsExtra{
Connections: 8,
},
})
if err != nil {
t.Fatal(err)
}
err = fetcher.Start()
if err != nil {
t.Fatal(err)
}
err = fetcher.Wait()
if err != nil {
t.Fatal(err)
}
// Verify file content
want := test.FileMd5(test.BuildFile)
got := test.FileMd5(test.DownloadFile)
if want != got {
t.Errorf("Download() got = %v, want %v", got, want)
}
os.Remove(test.DownloadFile)
})
}
// TestFetcher_RetryAfterError tests that the fetcher can retry downloading
// after a previous download attempt failed by calling Start() again.
func TestFetcher_RetryAfterError(t *testing.T) {
os.Remove(test.DownloadFile)
// Server fails first 3 requests (after resolve), then recovers
// With 1 connection and 3 retries:
// - First Start(): requests 2, 3, 4 → all fail (3 retries exhausted) → returns error
// - Second Start(): request 5 → succeeds (server recovered after 3 failures)
listener := test.StartTestFailThenRecoverServer(3)
defer listener.Close()
fetcher := buildFetcher()
err := fetcher.Resolve(&base.Request{
URL: "http://" + listener.Addr().String() + "/" + test.BuildName,
}, &base.Options{
Name: test.DownloadName,
Path: test.Dir,
Extra: &http.OptsExtra{
Connections: 1, // Use single connection to simplify test
},
})
if err != nil {
t.Fatal(err)
}
// First download attempt - should fail because server returns 416 after resolve
err = fetcher.Start()
if err != nil {
t.Fatal(err)
}
err = fetcher.Wait()
// First attempt should fail with 416 error
if err == nil {
t.Fatal("Expected first download attempt to fail, but it succeeded")
}
t.Logf("First attempt failed as expected: %v", err)
// Check that fetcher is in error state
state := fetcher.getState()
if state != stateError {
t.Errorf("Expected fetcher to be in stateError, got %v", state)
}
// Verify that we can call Start() again after error
// This tests the stateError handling in Start()
err = fetcher.Start()
if err != nil {
t.Fatalf("Start() after error failed: %v", err)
}
// Wait for second attempt - should succeed now that server has recovered
err = fetcher.Wait()
if err != nil {
t.Fatalf("Retry failed: %v", err)
}
// Verify file content
want := test.FileMd5(test.BuildFile)
got := test.FileMd5(test.DownloadFile)
if want != got {
t.Errorf("Download() got = %v, want %v", got, want)
}
os.Remove(test.DownloadFile)
}
func TestFetcherManager_ParseName(t *testing.T) {
type args struct {
u string