Skip to content
Control Plane Labs

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.docx

What 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