webdav: fix "XML syntax error" on paths which don't exist with ownCloud 10.16

ownCloud 10.16 answers a PROPFIND for a path which doesn't exist with
a 207 Multi-Status response, rather than a 404, with a body which
starts a multistatus document then appends a Sabre NotFound error to
it. This isn't valid XML so rclone failed with

    read metadata failed: XML syntax error on line 2: expected attribute name in element

whenever it was pointed at a directory which didn't exist yet.

Detect the NotFound error in the response and treat it as a 404.
This commit is contained in:
Nick Craig-Wood committed 2026-10-05 17:39:34 +01:00
1 parent be8b238033
commit f217c8a2aa
2 files changed
+83 -2

No files matched your search

+37 -2
View File
@@ -345,6 +345,41 @@ func itemIsDir(item *api.Response) bool {
return false
}
// sabreNotFound is the exception SabreDAV based servers report for a
// path which doesn't exist
var sabreNotFound = []byte(`Sabre\DAV\Exception\NotFound`)
// callPropfind does the PROPFIND in opts and decodes the response into result
//
// ownCloud 10.16 starts a 207 Multi-Status response before it finds
// that the path doesn't exist, then appends a Sabre NotFound error to
// it which makes it invalid XML. This is returned as the 404 *api.Error
// which servers normally give for a path which doesn't exist.
func (f *Fs) callPropfind(ctx context.Context, opts *rest.Opts, result *api.Multistatus) (resp *http.Response, err error) {
resp, err = f.srv.Call(ctx, opts)
if err != nil {
return resp, err
}
// Keep the start of the body to look for the error in
start := make([]byte, 4096)
n, _ := io.ReadFull(resp.Body, start)
start = start[:n]
body := resp.Body
resp.Body = struct {
io.Reader
io.Closer
}{io.MultiReader(bytes.NewReader(start), body), body}
err = rest.DecodeXML(resp, result)
if err != nil && bytes.Contains(start, sabreNotFound) {
return resp, &api.Error{
Exception: string(sabreNotFound),
Status: http.StatusText(http.StatusNotFound),
StatusCode: http.StatusNotFound,
}
}
return resp, err
}
// readMetaDataForPath reads the metadata from the path
func (f *Fs) readMetaDataForPath(ctx context.Context, path string) (info *api.Prop, err error) {
// FIXME how do we read back additional properties?
@@ -366,7 +401,7 @@ func (f *Fs) readMetaDataForPath(ctx context.Context, path string) (info *api.Pr
var resp *http.Response
err = f.pacer.Call(func() (bool, error) {
var attempt api.Multistatus
resp, err = f.srv.CallXML(ctx, &opts, nil, &attempt)
resp, err = f.callPropfind(ctx, &opts, &attempt)
if err == nil {
result = attempt
}
@@ -820,7 +855,7 @@ func (f *Fs) listAll(ctx context.Context, dir string, directoriesOnly bool, file
var resp *http.Response
err = f.pacer.Call(func() (bool, error) {
var attempt api.Multistatus
resp, err = f.srv.CallXML(ctx, &opts, nil, &attempt)
resp, err = f.callPropfind(ctx, &opts, &attempt)
if err == nil {
result = attempt
}
+46
View File
@@ -354,3 +354,49 @@ func TestListAllRetryDoesNotConcatenate(t *testing.T) {
}
assert.ElementsMatch(t, want, remotes)
}
// TestNotFoundInMultistatus checks that a missing path is detected when the
// server reports it by appending a Sabre NotFound error to a 207 response it
// has already started, as ownCloud 10.16 does, rather than by returning 404.
func TestNotFoundInMultistatus(t *testing.T) {
head := `<?xml version="1.0"?>` + "\n" + `<d:multistatus xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" xmlns:oc="http://owncloud.org/ns"`
root := head + `><d:response><d:href>/</d:href><d:propstat><d:prop><d:resourcetype><d:collection/></d:resourcetype></d:prop><d:status>HTTP/1.1 200 OK</d:status></d:propstat></d:response></d:multistatus>`
notFound := head + `<?xml version="1.0" encoding="utf-8"?>
<d:error xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns">
<s:exception>Sabre\DAV\Exception\NotFound</s:exception>
<s:message>File with name missing could not be located</s:message>
</d:error>
`
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, "PROPFIND", r.Method)
w.Header().Set("Content-Type", "application/xml; charset=utf-8")
w.WriteHeader(207)
body := notFound
if r.URL.Path == "/" {
body = root
}
_, err := fmt.Fprint(w, body)
require.NoError(t, err)
}))
defer ts.Close()
configfile.Install()
m := configmap.Simple{
"type": "webdav",
"url": ts.URL,
}
ctx := context.Background()
// A root which doesn't exist yet
_, err := webdav.NewFs(ctx, remoteName, "missing", m)
require.NoError(t, err)
f, err := webdav.NewFs(ctx, remoteName, "", m)
require.NoError(t, err)
_, err = f.NewObject(ctx, "missing")
assert.Equal(t, fs.ErrorObjectNotFound, err)
_, err = f.List(ctx, "missing")
assert.Equal(t, fs.ErrorDirNotFound, err)
}