diff --git a/weed/operation/upload_content.go b/weed/operation/upload_content.go index f469b2273..0775ca4d8 100644 --- a/weed/operation/upload_content.go +++ b/weed/operation/upload_content.go @@ -371,7 +371,16 @@ func (uploader *Uploader) upload_content(ctx context.Context, fillBufferFunction } else { reqReader = bytes.NewReader(option.BytesBuffer.Bytes()) } - req, postErr := http.NewRequest(http.MethodPost, option.UploadUrl, reqReader) + // Ensure the request will not hang indefinitely: if no deadline is set on ctx, + // apply a conservative default timeout. + ctxWithTimeout := ctx + if _, hasDeadline := ctx.Deadline(); !hasDeadline { + var cancel context.CancelFunc + // Default timeout chosen to be generous for large uploads but finite to avoid hangs. + ctxWithTimeout, cancel = context.WithTimeout(ctx, 10*time.Minute) + defer cancel() + } + req, postErr := http.NewRequestWithContext(ctxWithTimeout, http.MethodPost, option.UploadUrl, reqReader) if postErr != nil { glog.V(1).InfofCtx(ctx, "create upload request %s: %v", option.UploadUrl, postErr) return nil, fmt.Errorf("create upload request %s: %v", option.UploadUrl, postErr) diff --git a/weed/operation/upload_content_test.go b/weed/operation/upload_content_test.go new file mode 100644 index 000000000..326aff619 --- /dev/null +++ b/weed/operation/upload_content_test.go @@ -0,0 +1,61 @@ +package operation + +import ( + "bytes" + "context" + "io" + "net/http" + "net/http/httptest" + "testing" + "time" +) + +// Test that Upload respects request context timeout and does not hang indefinitely +func TestUploadTimesOutOnStalledConnectionViaContext(t *testing.T) { + // Create a test server that stalls and never writes a response + ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Do not read the body and do not write a response; just stall + select { + case <-time.After(10 * time.Second): + case <-r.Context().Done(): + // Proactively close the underlying connection to avoid server Close delay + if hj, ok := w.(http.Hijacker); ok { + if conn, _, err := hj.Hijack(); err == nil { + _ = conn.Close() + } + } + } + })) + // Make the server's own timeouts aggressive so Close does not block long + ts.Config.ReadTimeout = 200 * time.Millisecond + ts.Config.WriteTimeout = 200 * time.Millisecond + ts.Config.IdleTimeout = 200 * time.Millisecond + ts.Start() + defer ts.Close() + + u, err := NewUploader() + if err != nil { + t.Fatalf("failed to create uploader: %v", err) + } + + // Short timeout to make the test fast + ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond) + defer cancel() + + data := bytes.Repeat([]byte("a"), 1024) + // Call the lower-level upload to avoid internal retries and keep test fast + _, err = u.upload_content(ctx, func(w io.Writer) error { + _, writeErr := w.Write(data) + return writeErr + }, len(data), &UploadOption{ + UploadUrl: ts.URL + "/upload", + Filename: "test.bin", + MimeType: "application/octet-stream", + }) + + if err == nil { + t.Fatalf("expected timeout error, got nil") + } +} + +