diff --git a/backend/webdav/webdav.go b/backend/webdav/webdav.go index e3eac5239..29ad1cdcf 100644 --- a/backend/webdav/webdav.go +++ b/backend/webdav/webdav.go @@ -1546,6 +1546,18 @@ func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (in io.Read } err = o.fs.pacer.Call(func() (bool, error) { resp, err = o.fs.srv.Call(ctx, &opts) + if err == nil { + err = rest.CheckContentRange(resp, options, o.size) + if err != nil { + if resp != nil && resp.Body != nil { + _ = resp.Body.Close() + } + if errors.Is(err, fs.ErrorRangeIgnored) { + return false, err + } + return true, fserrors.RetryError(err) + } + } return o.fs.shouldRetry(ctx, resp, err) }) if err != nil { diff --git a/backend/webdav/webdav_internal_test.go b/backend/webdav/webdav_internal_test.go index 2aa7acd48..dfdd38ef5 100644 --- a/backend/webdav/webdav_internal_test.go +++ b/backend/webdav/webdav_internal_test.go @@ -3,9 +3,11 @@ package webdav_test import ( "context" "fmt" + "io" "net/http" "net/http/httptest" "strings" + "sync/atomic" "testing" "github.com/rclone/rclone/backend/webdav" @@ -151,3 +153,90 @@ func TestReservedCharactersInPathAreEscaped(t *testing.T) { assert.Contains(t, capturedPath, "my%3Btest", "semicolons in path should be percent-encoded") assert.NotContains(t, capturedPath, "my;test", "raw semicolons should not appear in path") } + +const fileInfoResponse = ` + + /file.txt + + + 10 + + + HTTP/1.1 200 OK + + +` + +func prepareFileObject(ctx context.Context, t *testing.T, getHandler http.HandlerFunc) (fs.Object, func()) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == "PROPFIND" { + w.WriteHeader(http.StatusMultiStatus) + _, err := fmt.Fprint(w, fileInfoResponse) + require.NoError(t, err) + return + } + if r.Method == http.MethodGet { + getHandler(w, r) + return + } + http.Error(w, "unexpected method", http.StatusMethodNotAllowed) + }) + ts := httptest.NewServer(handler) + + configfile.Install() + f, err := webdav.NewFs(ctx, remoteName, "", configmap.Simple{ + "type": "webdav", + "url": ts.URL, + }) + require.NoError(t, err) + o, err := f.NewObject(ctx, "file.txt") + require.NoError(t, err) + return o, ts.Close +} + +func TestOpenDoesNotRetryIgnoredRange(t *testing.T) { + var getRequests atomic.Int32 + o, tidy := prepareFileObject(context.Background(), t, func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "bytes=2-4", r.Header.Get("Range")) + getRequests.Add(1) + w.Header().Set("Content-Length", "10") + w.WriteHeader(http.StatusOK) + _, err := io.WriteString(w, "abcdefghij") + require.NoError(t, err) + }) + defer tidy() + + in, err := o.Open(context.Background(), &fs.RangeOption{Start: 2, End: 4}) + assert.Nil(t, in) + assert.ErrorIs(t, err, fs.ErrorRangeIgnored) + assert.Equal(t, int32(1), getRequests.Load()) +} + +func TestOpenRetriesMismatchedContentRange(t *testing.T) { + var getRequests atomic.Int32 + o, tidy := prepareFileObject(context.Background(), t, func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "bytes=2-4", r.Header.Get("Range")) + if getRequests.Add(1) == 1 { + w.Header().Set("Content-Length", "3") + w.Header().Set("Content-Range", "bytes 0-2/10") + w.WriteHeader(http.StatusPartialContent) + _, err := io.WriteString(w, "abc") + require.NoError(t, err) + return + } + w.Header().Set("Content-Length", "3") + w.Header().Set("Content-Range", "bytes 2-4/10") + w.WriteHeader(http.StatusPartialContent) + _, err := io.WriteString(w, "cde") + require.NoError(t, err) + }) + defer tidy() + + in, err := o.Open(context.Background(), &fs.RangeOption{Start: 2, End: 4}) + require.NoError(t, err) + defer func() { require.NoError(t, in.Close()) }() + contents, err := io.ReadAll(in) + require.NoError(t, err) + assert.Equal(t, "cde", string(contents)) + assert.Equal(t, int32(2), getRequests.Load()) +}