package downloader_test import ( "context" "net/http" "net/http/httptest" "os" "time" . "github.com/mudler/LocalAI/pkg/downloader" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) var _ = Describe("Download stall timeout", func() { var filePath string var savedTimeout time.Duration BeforeEach(func() { dir, err := os.Getwd() Expect(err).ToNot(HaveOccurred()) filePath = dir + "/stall_model" savedTimeout = DownloadStallTimeout }) AfterEach(func() { DownloadStallTimeout = savedTimeout _ = os.Remove(filePath) _ = os.Remove(filePath + ".partial") }) It("aborts a download that stalls mid-stream instead of hanging forever", func() { // Server sends a chunk, flushes, then blocks forever without closing // the connection — a silently-dropped TCP stream. Without a stall // guard the body Read blocks indefinitely and DownloadFile never // returns. release := make(chan struct{}) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method == "HEAD" { w.Header().Set("Accept-Ranges", "bytes") w.WriteHeader(http.StatusOK) return } w.WriteHeader(http.StatusOK) _, _ = w.Write(make([]byte, 4096)) if f, ok := w.(http.Flusher); ok { f.Flush() } <-release // hang: no more data, never close })) defer server.Close() defer close(release) DownloadStallTimeout = 300 * time.Millisecond done := make(chan error, 1) go func() { done <- URI(server.URL).DownloadFileWithContext( context.Background(), filePath, "", 1, 1, func(s1, s2, s3 string, f float64) {}) }() var err error Eventually(done, "5s").Should(Receive(&err)) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("stall")) }) It("preserves the .partial file when a download stalls so it can resume", func() { release := make(chan struct{}) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method == "HEAD" { w.Header().Set("Accept-Ranges", "bytes") w.WriteHeader(http.StatusOK) return } w.WriteHeader(http.StatusOK) _, _ = w.Write(make([]byte, 4096)) if f, ok := w.(http.Flusher); ok { f.Flush() } <-release })) defer server.Close() defer close(release) DownloadStallTimeout = 300 * time.Millisecond done := make(chan error, 1) go func() { done <- URI(server.URL).DownloadFileWithContext( context.Background(), filePath, "", 1, 1, func(s1, s2, s3 string, f float64) {}) }() Eventually(done, "5s").Should(Receive(HaveOccurred())) info, statErr := os.Stat(filePath + ".partial") Expect(statErr).ToNot(HaveOccurred(), "the .partial must survive a stall so the next attempt can resume") Expect(info.Size()).To(BeNumerically(">", 0)) }) It("does not abort a slow-but-steady download", func() { // One byte every 100ms keeps the idle clock from ever expiring even // though the total transfer outlasts the stall timeout. payload := make([]byte, 12) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method == "HEAD" { w.Header().Set("Accept-Ranges", "bytes") w.WriteHeader(http.StatusOK) return } w.WriteHeader(http.StatusOK) f, _ := w.(http.Flusher) for i := range payload { _, _ = w.Write(payload[i : i+1]) if f != nil { f.Flush() } time.Sleep(100 * time.Millisecond) } })) defer server.Close() DownloadStallTimeout = 300 * time.Millisecond err := URI(server.URL).DownloadFileWithContext( context.Background(), filePath, "", 1, 1, func(s1, s2, s3 string, f float64) {}) Expect(err).ToNot(HaveOccurred()) }) })