Fix non-range download bug: downloaded bytes exceeding total file size

When HTTP server doesn't return Content-Length or doesn't support Range
requests, the download can accumulate extra bytes on pause/resume because:
1. Each retry/resume re-downloads from the beginning but writes at the
   accumulated offset
2. No size limit enforcement for non-range downloads
3. onDownloadComplete incorrectly uses chunk.remain() for non-range
   downloads, which gives negative values for chunk(0,0)

Fixes:
- Reset chunk state in downloadChunkOnce and runConnectionFallback for
  non-range downloads since server will send from beginning
- Add size limit checks in download loops when Size is known
- Fix onDownloadComplete to use conn.Completed for non-range downloads
  instead of unreliable chunk.remain()
- Add test cases for non-range download, pause/resume, and size limit

Co-authored-by: monkeyWie <[email protected]>
This commit is contained in:
copilot-swe-agent[bot]andmonkeyWie committed 2026-03-11 02:50:48 +00:00
1 parent 27c0f81a1e
commit 2daa9c45df
3 files changed
+197 -9

No files matched your search

+93
View File
@@ -307,6 +307,99 @@ func TestFetcher_DownloadChunked(t *testing.T) {
downloadNormal(listener, 2, t)
}
func TestFetcher_DownloadChunkedContinue(t *testing.T) {
listener := test.StartTestCustomServer()
defer listener.Close()
downloadContinue(listener, 1, t)
}
func TestFetcher_DownloadNoRangeWithSize(t *testing.T) {
listener := test.StartTestNoRangeServer()
defer listener.Close()
downloadNormal(listener, 1, t)
}
func TestFetcher_DownloadNoRangeContinue(t *testing.T) {
listener := test.StartTestNoRangeServer()
defer listener.Close()
downloadContinue(listener, 1, t)
}
// TestFetcher_DownloadNoRangeNotExceedSize verifies that when a server returns
// Content-Length but doesn't support Range requests, the downloaded bytes never
// exceed the total file size after pause and resume.
func TestFetcher_DownloadNoRangeNotExceedSize(t *testing.T) {
listener := test.StartTestNoRangeServer()
defer listener.Close()
fetcher := downloadReady(listener, 1, t)
err := fetcher.Start()
if err != nil {
t.Fatal(err)
}
// Let it download some data
time.Sleep(time.Millisecond * 50)
if err := fetcher.Pause(); err != nil {
t.Fatal(err)
}
time.Sleep(time.Millisecond * 50)
if err := fetcher.Start(); err != nil {
t.Fatal(err)
}
err = fetcher.Wait()
if err != nil {
t.Fatal(err)
}
// Verify total downloaded does not exceed file size
totalDownloaded := fetcher.Progress().TotalDownloaded()
if totalDownloaded > test.BuildSize {
t.Errorf("Downloaded %d bytes exceeds file size %d", totalDownloaded, test.BuildSize)
}
want := test.FileMd5(test.BuildFile)
got := test.FileMd5(test.DownloadFile)
if want != got {
t.Errorf("Download() got = %v, want %v", got, want)
}
}
// TestFetcher_DownloadChunkedNotExceedSize verifies that when a server doesn't
// return Content-Length (chunked transfer), the download properly handles
// pause/resume without accumulating extra bytes.
func TestFetcher_DownloadChunkedNotExceedSize(t *testing.T) {
listener := test.StartTestCustomServer()
defer listener.Close()
fetcher := downloadReady(listener, 1, t)
err := fetcher.Start()
if err != nil {
t.Fatal(err)
}
// Let it download some data
time.Sleep(time.Millisecond * 50)
if err := fetcher.Pause(); err != nil {
t.Fatal(err)
}
time.Sleep(time.Millisecond * 50)
if err := fetcher.Start(); err != nil {
t.Fatal(err)
}
err = fetcher.Wait()
if err != nil {
t.Fatal(err)
}
want := test.FileMd5(test.BuildFile)
got := test.FileMd5(test.DownloadFile)
if want != got {
t.Errorf("Download() got = %v, want %v", got, want)
}
}
func TestFetcher_DownloadPost(t *testing.T) {
listener := test.StartTestPostServer()
defer listener.Close()