Skip to content
Control Plane Labs

207 Multi-Status

Success response defined by RFC 4918 §11.1.

Last updated July 27, 2026

Common causes at a glance

  • A PROPFIND enumerating a collection with per-resource property results
  • A PROPPATCH where some property updates succeeded and others failed
  • A COPY or MOVE across a tree where individual members were locked or missing
  • A CalDAV or CardDAV client synchronising a calendar or address book collection

Reproduce it

curl -i -X PROPFIND -H 'Depth: 1' -H 'Content-Type: application/xml' https://dav.example.com/coll/

What 207 Multi-Status means

A WebDAV method like PROPFIND, PROPPATCH, COPY, or MOVE can act on a whole collection tree. One HTTP status cannot describe “twelve succeeded, one is locked, one does not exist”, so WebDAV returns 207 with a DAV:multistatus document in the body carrying a DAV:response element per resource, each with its own status line.

A client that only checks the HTTP status therefore treats a request where every single operation failed as a success. Parsing the XML is mandatory, and per-resource failures have to be surfaced by the client.

What the spec says

RFC 4918 §11.1 defines the code and points at §13 for the response format: application/xml or text/xml containing a DAV:multistatus root. Each DAV:response carries either a single DAV:status or, for PROPFIND, one or more DAV:propstat elements grouping properties by the status that applies to them (§9.1.2). §13.2 lets a server omit descendants whose status matches an ancestor’s, so a missing resource in the document does not mean it was skipped. Extensions add codes inside the document: 208 from RFC 5842, and 424 Failed Dependency (§11.4) after a failed PROPPATCH.

What actually causes it

Almost every 207 problem is a parsing problem. Clients hardcode the D: prefix instead of resolving the DAV: namespace URI, then break the day a server emits a different prefix or a default namespace. Others read only the first DAV:response, or ignore DAV:propstat grouping and treat a 404 propstat block as a present-but-empty property. Servers contribute non-UTF-8 XML, text/html error pages inside what claims to be a multistatus, and collection hrefs missing the trailing slash so client path matching fails. Nginx’s DAV module does not implement PROPFIND at all, so a WebDAV client hitting an nginx-only deployment gets 405 rather than 207 and the confusion starts there.

How to debug it

Get the raw XML out first, then look at the per-resource statuses rather than the 207:

curl -s -X PROPFIND -H 'Depth: 1' -H 'Content-Type: application/xml' \
--data '<?xml version="1.0"?><propfind xmlns="DAV:"><allprop/></propfind>' \
https://dav.example.com/coll/ | xmllint --format -

Extract every status line in the document; that is where the failures are. If the count of DAV:response elements is lower than the number of members you expect, check Depth: the default when the header is absent is infinity, and many servers refuse infinite depth outright. Confirm the server speaks WebDAV with an OPTIONS request and look for the DAV response header (§10.1); if it is missing, no amount of XML will help.

Working examples

curl

# Enumerate one level and pretty-print the multistatus document.
curl -s -X PROPFIND \
  -H 'Depth: 1' \
  -H 'Content-Type: application/xml' \
  --data '<?xml version="1.0"?><propfind xmlns="DAV:"><allprop/></propfind>' \
  https://dav.example.com/coll/ | xmllint --format -

# The statuses that matter are inside the body, not on the response:
curl -s -X PROPFIND -H 'Depth: 1' https://dav.example.com/coll/ \
  | grep -oE '<[A-Za-z]+:?status>[^<]*' | sort | uniq -c

# Confirm the server speaks WebDAV at all:
curl -sI -X OPTIONS https://dav.example.com/coll/ | grep -i '^dav:'

Python (requests)

import xml.etree.ElementTree as ET
import requests

DAV = "{DAV:}"  # resolve the namespace URI; never trust the prefix

resp = requests.request(
    "PROPFIND",
    "https://dav.example.com/coll/",
    headers={"Depth": "1", "Content-Type": "application/xml"},
    data='<?xml version="1.0"?><propfind xmlns="DAV:"><allprop/></propfind>',
    timeout=30,
)
print(resp.status_code)  # 207 - says nothing about the individual results

root = ET.fromstring(resp.content)
for r in root.findall(f"{DAV}response"):
    href = r.findtext(f"{DAV}href")
    statuses = [s.text for s in r.iter(f"{DAV}status")]
    print(href, statuses)
    if any("200" not in (s or "") for s in statuses):
        print("  -> partial failure on this resource")

Node (fetch)

// fetch() is fine here; the work is parsing the XML body.
const res = await fetch("https://dav.example.com/coll/", {
  method: "PROPFIND",
  headers: { Depth: "1", "Content-Type": "application/xml" },
  body: '<?xml version="1.0"?><propfind xmlns="DAV:"><allprop/></propfind>',
});

console.log(res.status); // 207

const xml = await res.text();
const doc = new DOMParser().parseFromString(xml, "application/xml");

// getElementsByTagNameNS avoids depending on the D: prefix the server chose.
for (const r of doc.getElementsByTagNameNS("DAV:", "response")) {
  const href = r.getElementsByTagNameNS("DAV:", "href")[0]?.textContent;
  const statuses = [...r.getElementsByTagNameNS("DAV:", "status")]
    .map((s) => s.textContent);
  console.log(href, statuses);
}

Reference and tooling

The authoritative list of assigned status codes is theIANA HTTP Status Code Registry. For a searchable copy with the semantics summarised, use theHTTP status code reference on this site. To see exactly what a server is sending, theheader inspector shows the raw status line and response headers, and thecurl builder assembles the request flags for you.

Other 2xx codes