423 Locked
Client error response defined by RFC 4918 §11.3.
Last updated July 27, 2026
Common causes at a glance
- A write to a resource another WebDAV client holds an exclusive lock on
- Missing or stale lock token in the If header on PUT, DELETE, MOVE or PROPPATCH
- An ancestor collection locked with Depth: infinity
- A stale lock left behind by a crashed client that never sent UNLOCK
Reproduce it
curl -i -X PUT --data-binary @report.docx https://dav.example.com/docs/report.docxWhat 423 Locked means
WebDAV added write locks so two people editing the same document over HTTP
do not silently clobber each other. A client sends LOCK, receives a lock
token, and must then include that token in an If header on every write to the
locked resource. A write without the token, or with the wrong one, gets 423.
The lock can also be indirect. Locking a collection with Depth: infinity
locks every member, so a PUT to a file that was never locked directly still
fails if an ancestor collection is held. That is why 423 sometimes points at a path
several levels above the one in the request line.
What the spec says
RFC 4918 §11.3 defines 423 and allows the response body
to carry a lock-token-submitted or no-conflicting-lock
precondition element naming the URL that is actually locked. The lock model is
§6, the LOCK method is §9.10, and
the If header used to submit a token is §10.4. Note
what the spec does not say: locks are not access control.
§6.4 is explicit that a lock only prevents accidental overwrite, and
anyone holding the token can use it.
What actually causes it
The dominant real-world cause is a client that died holding a lock.
Word, Finder, and the various Nextcloud and SharePoint desktop agents take locks routinely
and are not always careful about releasing them; a laptop that sleeps mid-edit leaves an
exclusive lock on the server until its timeout expires. Because
§6.6 makes the timeout a client hint rather than a guarantee, some
implementations hold locks for hours. The other frequent cause is a client that took a
lock correctly and then failed to echo the token, usually because a proxy stripped the
If header in transit — §10.4.5 warns about exactly
that.
How to debug it
Ask the server who holds the lock. A PROPFIND for
lockdiscovery returns the active lock, its owner, its depth, and its
remaining timeout:
curl -sS -X PROPFIND -H 'Depth: 0' -H 'Content-Type: application/xml' \
--data '<?xml version="1.0"?><propfind xmlns="DAV:"><prop><lockdiscovery/></prop></propfind>' \
https://dav.example.com/docs/report.docx
Run it against the parent collection too, because a depth-infinity lock upstream will not
appear in the child’s own lock discovery but still blocks the write. Once you have the
token, resubmit with -H 'If: (<opaquelocktoken:...>)'. If the lock is
stale, UNLOCK with that token clears it; most servers also let an
administrator break a lock out of band. Do not treat 423 as retryable — backing off just
waits out the timeout.
Working examples
curl
# 1. Find out who holds the lock.
curl -sS -X PROPFIND -H 'Depth: 0' -H 'Content-Type: application/xml' \
--data '<?xml version="1.0"?><propfind xmlns="DAV:"><prop><lockdiscovery/></prop></propfind>' \
https://dav.example.com/docs/report.docx
# 2. Retry the write with the token in an If header.
curl -i -X PUT \
-H 'If: (<opaquelocktoken:e71d4fae-5dec-22d6-fea5-00a0c91e6be4>)' \
--data-binary @report.docx \
https://dav.example.com/docs/report.docx
Python (requests)
import requests
url = "https://dav.example.com/docs/report.docx"
resp = requests.put(url, data=open("report.docx", "rb"), timeout=30)
if resp.status_code == 423:
# Discover the active lock rather than retrying blindly.
body = ('<?xml version="1.0"?><propfind xmlns="DAV:">'
"<prop><lockdiscovery/></prop></propfind>")
info = requests.request("PROPFIND", url, data=body,
headers={"Depth": "0"}, timeout=30)
print(info.text) # contains <locktoken><href>opaquelocktoken:...</href>
Node (fetch)
const url = "https://dav.example.com/docs/report.docx";
const token = "opaquelocktoken:e71d4fae-5dec-22d6-fea5-00a0c91e6be4";
const res = await fetch(url, {
method: "PUT",
headers: { If: `(<${token}>)` },
body: "file contents",
});
// 423 is not transient. Surface it to a human instead of scheduling a retry.
if (res.status === 423) console.error("locked:", await res.text());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 4xx codes
- 400 Bad Request
- 401 Unauthorized
- 402 Payment Required
- 403 Forbidden
- 404 Not Found
- 405 Method Not Allowed
- 406 Not Acceptable
- 407 Proxy Authentication Required
- 408 Request Timeout
- 409 Conflict
- 410 Gone
- 411 Length Required
- 412 Precondition Failed
- 413 Content Too Large
- 414 URI Too Long
- 415 Unsupported Media Type
- 416 Range Not Satisfiable
- 417 Expectation Failed
- 418 (Unused)
- 419 Page Expired
- 421 Misdirected Request
- 422 Unprocessable Content
- 424 Failed Dependency
- 425 Too Early
- 426 Upgrade Required
- 428 Precondition Required
- 429 Too Many Requests
- 431 Request Header Fields Too Large
- 444 No Response
- 451 Unavailable For Legal Reasons
- 494 Request Header Too Large
- 495 SSL Certificate Error
- 496 SSL Certificate Required
- 497 HTTP Request Sent to HTTPS Port
- 499 Client Closed Request