You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
[Bug]: PROPFIND on /remote.php/webdav returns 207 instead of 404 for non-existent paths since 33.0.7 (streamed multistatus commits the status code before the node is resolved) #63415
PROPFIND against the legacy WebDAV endpoint /remote.php/webdav/<anything> returns 207 Multi-Status for every path, including paths that do not exist. It should return 404 Not Found. The modern endpoint /remote.php/dav/files/<uid>/... is unaffected and
still returns 404.
This breaks Windows mapped network drives. Explorer's "New Folder" flow probes for a free
name with PROPFIND and only sends MKCOL once it gets a 404. Since the legacy endpoint
never answers 404, Explorer concludes every candidate name is taken, never sends MKCOL,
and throws its own "Access denied" dialog. No 403 is ever returned, and — this is the fun
part — nothing whatsoever is written to nextcloud.log, because ExceptionLoggerPlugin
classifies NotFound as non-fatal. So from the server's point of view the request is a
complete success, and from the user's point of view the share is broken. Enjoy explaining
that one to your users.
I lost most of a day to this before giving up on the "obviously it's a permissions problem"
theory and reading the diff. Root cause, for whoever gets to fix it:
Introduced by commit ab34c4c21884"perf(dav): Stream PROPFIND output", backported to
stable33 as #62225, first shipped in 33.0.7. It is the only occurrence of that flag
in the entire tree, and it is set unconditionally.
The chain (sabre/dav 4.7.0, as vendored):
CorePlugin::httpPropFind() line 327 calls Server::getPropertiesIteratorForPath().
That method contains yield, so it is a generator — calling it executes none of its
body. The $this->tree->getNodeForPath($path) inside it (Server.php:971) never runs at
this point.
Line 330, immediately after: $response->setStatus(207). Unconditionally, with nothing
resolved and nothing validated.
Line 346: generateMultiStatus(). With streaming enabled (Server.php:1641-1646) this
returns a closure rather than a serialized body. Still nothing iterated.
Server::invokeMethod() line 490 → Sapi::sendResponse() line 64 flushes the 207
status line and headers, then line 81 invokes the closure.
Now the generator finally runs, ObjectTree::getNodeForPath() throws Sabre\DAV\Exception\NotFound as it always has, and the catch block in OCA\DAV\Connector\Sabre\Server::start() dutifully sets the status to 404 and re-sends —
into a response whose headers left the building one stack frame ago.
So the 404 logic is fine. It just runs after the status code has already been committed to
the wire. The body that comes back is a degenerate multistatus: writeMultiStatus() gets as
far as the XML prolog and the opening <d:multistatus>, then the first loop iteration
throws, $w->flush() never runs, and the d:error document from the exception handler is
appended on top. That is why the responses are a uniform ~1150–1350 bytes regardless of
which non-existent path you ask for.
Affected endpoints — everything routed through ServerFactory:
Endpoint
Entry point
Affected
/remote.php/webdav
apps/dav/appinfo/v1/webdav.php
❌ yes
/remote.php/files
same file (remote.php:87)
❌ yes
/public.php/webdav
apps/dav/appinfo/v1/publicwebdav.php
❌ yes
/public.php/dav
apps/dav/appinfo/v2/publicremote.php
❌ yes
/remote.php/dav
apps/dav/appinfo/v2/remote.php → OCA\DAV\Server
✅ no
/remote.php/dav never touches the flag, which is exactly why the modern endpoint still
behaves. That asymmetry is the whole tell.
I also checked whether any plugin on the legacy server happens to resolve the node during beforeMethod:PROPFIND and would incidentally save us. None does: LockPlugin returns
early unless the method is PUT, ViewOnlyPlugin only hooks GET/COPY/MOVE, and FilesPlugin hooks propFind, which fires inside the generator and is therefore also too
late. Nothing catches this.
Version status:
Version
streamMultiStatus present
32.0.13
no
33.0.5
no
33.0.6
no
33.0.7
yes ← regression enters
34.0.3
yes
master, stable33, stable34 (checked at time of writing)
yes
Reproduced live on 33.0.7.1. I then pulled the 34.0.3 release tarball and audited it
line-by-line specifically to find out whether upgrading would save me. It would not.
Steps to reproduce
You need an app password — SAML/SSO accounts can't do basic auth against WebDAV.
Create an app password for a test account (Settings → Security → Devices & sessions).
PROPFIND a path that definitively does not exist, on both endpoints:
Optional, the way real users hit it: map a network drive in Windows Explorer to https://cloud.example.com/remote.php/webdav, then right-click → New → Folder.
The folder is never created; Explorer shows "Access denied". The access log shows a burst
of PROPFIND … 207 for Neuer Ordner / New folder and desktop.ini, and no MKCOL is ever sent, because Explorer is still waiting for a 404 that will never come:
203.0.113.10 [.../...] "PROPFIND /remote.php/webdav/Foo/Bar" 207 1334
203.0.113.10 [.../...] "PROPFIND /remote.php/webdav/Foo/Bar/Neuer%20Ordner" 207 1155 <- should be 404
203.0.113.10 [.../...] "PROPFIND /remote.php/webdav/Foo/Bar/desktop.ini" 207 1154 <- should be 404
For comparison, the same operation against /remote.php/dav/files/<uid>/ on the same server,
same minute, working exactly as intended:
If you want the aggregate version: grep your access log for "PROPFIND /remote.php/webdav. On 33.0.6 you get a steady stream of 404s every single
day. From the moment 33.0.7 is installed you get exactly zero, forever. In my case
that was ~265 000 legacy PROPFINDs across twelve days without a single 404. The cutover
is visible to the second in the log, matched against the updater timestamp.
Expected behavior
PROPFIND on a non-existent path returns 404 Not Found on the legacy endpoint, the same
way it does on /remote.php/dav and the same way it did in 33.0.6 and every release before
it. 207 Multi-Status should mean the resource exists and here are its properties, not
"I already sent the headers, sorry".
Concretely: the node must be resolved before the status code is committed. The streaming
optimisation is fine in principle, it just needs to prime the generator first. Priming with
a single ->current() on the iterator before setStatus(207) runs the generator up to its
first yield, which triggers getNodeForPath() and lets NotFound propagate while the
status is still mutable — and a subsequent foreach still works, since the generator has not
advanced past the first yield. That preserves the perf win for the normal case.
Failing that: make the flag opt-in via a config value, or revert the line on the stable
branches until it can be done safely. Silently converting every 404 into a 207 on an endpoint
that Windows depends on is not a great trade for saving some memory on PROPFIND.
Nextcloud Server version
34
Operating system
Debian/Ubuntu
PHP engine version
PHP 8.3
Web server
Apache (supported)
Database engine version
MariaDB
Is this bug present after an update or on a fresh install?
Fresh Nextcloud Server install
Are you using the Nextcloud Server Encryption module?
Enabled:
- activity: 6.0.0
- bruteforcesettings: 6.0.0
- calendar: 6.5.2
- circles: 33.0.0
- cloud_federation_api: 1.17.0
- comments: 1.23.0
- contacts: 8.7.5
- contactsinteraction: 1.14.1
- dav: 1.36.0
- federatedfilesharing: 1.23.0
- federation: 1.23.0
- files: 2.5.0
- files_antivirus: 6.3.2
- files_downloadlimit: 5.1.0
- files_pdfviewer: 6.0.0
- files_reminders: 1.6.0
- files_sharing: 1.25.2
- files_trashbin: 1.23.0
- files_versions: 1.26.0
- groupfolders: 21.0.13
- impersonate: 4.0.0
- logreader: 6.0.0
- lookup_server_connector: 1.21.0
- notifications: 6.0.0
- oauth2: 1.21.0
- onlyoffice: 10.1.2
- password_policy: 5.0.0
- photos: 6.0.0
- privacy: 5.0.0
- profile: 1.2.0
- provisioning_api: 1.23.0
- recommendations: 6.0.0
- related_resources: 4.0.0
- serverinfo: 5.0.0
- settings: 1.16.0
- sharebymail: 1.23.0
- survey_client: 5.0.0
- systemtags: 1.23.0
- text: 7.0.1
- theming: 2.8.0
- twofactor_backupcodes: 1.22.0
- twofactor_totp: 15.0.0
- updatenotification: 1.23.0
- user_saml: 8.2.0
- viewer: 6.0.0
- webhook_listeners: 1.5.0
- workflowengine: 2.15.0
Disabled:
- admin_audit: 1.23.0
- app_api: 33.0.0 (installed 32.0.0)
- dashboard: 7.13.0 (installed 7.0.0)
- encryption: 2.21.0
- files_external: 1.25.1
- firstrunwizard: 6.0.0 (installed 2.2.1)
- nextcloud_announcements: 5.0.0 (installed 1.5.0)
- sharelisting: 1.3.0 (installed 1.3.0)
- support: 5.0.0 (installed 4.0.0)
- suspicious_login: 11.0.0
- twofactor_nextcloud_notification: 7.0.0
- user_ldap: 1.24.0 (installed 1.23.0)
- user_status: 1.13.0 (installed 1.0.1)
- weather_status: 1.13.0 (installed 1.0.0)
Worth noting from mine, since they're the usual suspects and none of them are involved:- `groupfolders` is installed but **no group folders are configured**- `files_accesscontrol` is **not** installed- `files_external` is **not** enabled — the storage in question is a plain local home storage- share permissions on the affected folder are `31` (full)The bug reproduces on a brand-new empty path in the user's own home directory, so none of
this matters, but I'll save you the round trip of asking.
Nextcloud Signing status
No errors have been found.
Nextcloud Logs
(empty)That is not laziness — there is genuinely nothing to paste. `NotFound` is on the`nonFatalExceptions` list in
[`apps/dav/lib/Connector/Sabre/ExceptionLoggerPlugin.php`](https://github.com/nextcloud/server/blob/master/apps/dav/lib/Connector/Sabre/ExceptionLoggerPlugin.php),so the exception that *should* have produced the 404 is logged at debug level at most andthe request is otherwise recorded as a success. Zero log entries for the affected client IPacross the entire incident window, at default log level. This is a large part of why the bugis so annoying to track down: every observable signal says the server is healthy.
Additional info
Regression range: absent in 32.0.13, 33.0.5, 33.0.6 — present in 33.0.7. Bisected by
diffing ServerFactory.php across release tags.
Clients affected: anything that relies on 404 to test for existence before writing. Microsoft-WebDAV-MiniRedir (Windows mapped drives) is the obvious one and the one that
hurts, since it can no longer create folders or files at all. Anything doing PROPFIND-then-PUT against the legacy endpoint is presumably equally broken.
Not affected: the Nextcloud desktop/mobile clients, since they use /remote.php/dav.
Which is probably why this got through CI and a full point release.
Red herring, flagged so nobody else burns an afternoon on it: server-wide 403 counts
also collapse to zero at the same upgrade. That part is not this bug — most of those 403s were forbidden-filename blocking of desktop.ini, and that behaviour was changed
deliberately; those requests now correctly return 404 on the modern endpoint. "The 403s
came back" is not a fix signal for this issue. The only reliable signal is 404 returning
to the legacy endpoint.
Workaround for anyone finding this issue via search: remap affected clients from https://cloud.example.com/remote.php/webdav to https://cloud.example.com/remote.php/dav/files/<uid>/. Note that <uid> is the internal
user ID, which for SSO accounts may be a GUID rather than the login name — get it with occ user:info <login>. Alternatively, delete the one line at ServerFactory.php:83
locally, at the cost of the streaming optimisation and your integrity check.
I'm happy to test a patch against 33.x or 34.x on a staging instance.
Bug description
PROPFINDagainst the legacy WebDAV endpoint/remote.php/webdav/<anything>returns207 Multi-Statusfor every path, including paths that do not exist. It should return404 Not Found. The modern endpoint/remote.php/dav/files/<uid>/...is unaffected andstill returns
404.This breaks Windows mapped network drives. Explorer's "New Folder" flow probes for a free
name with
PROPFINDand only sendsMKCOLonce it gets a404. Since the legacy endpointnever answers
404, Explorer concludes every candidate name is taken, never sendsMKCOL,and throws its own "Access denied" dialog. No
403is ever returned, and — this is the funpart — nothing whatsoever is written to
nextcloud.log, becauseExceptionLoggerPluginclassifies
NotFoundas non-fatal. So from the server's point of view the request is acomplete success, and from the user's point of view the share is broken. Enjoy explaining
that one to your users.
I lost most of a day to this before giving up on the "obviously it's a permissions problem"
theory and reading the diff. Root cause, for whoever gets to fix it:
Server::$streamMultiStatus = true;inapps/dav/lib/Connector/Sabre/ServerFactory.php:83.Introduced by commit
ab34c4c21884"perf(dav): Stream PROPFIND output", backported tostable33 as #62225, first shipped in 33.0.7. It is the only occurrence of that flag
in the entire tree, and it is set unconditionally.
The chain (sabre/dav 4.7.0, as vendored):
CorePlugin::httpPropFind()line 327 callsServer::getPropertiesIteratorForPath().That method contains
yield, so it is a generator — calling it executes none of itsbody. The
$this->tree->getNodeForPath($path)inside it (Server.php:971) never runs atthis point.
$response->setStatus(207). Unconditionally, with nothingresolved and nothing validated.
generateMultiStatus(). With streaming enabled (Server.php:1641-1646) thisreturns a closure rather than a serialized body. Still nothing iterated.
Server::invokeMethod()line 490 →Sapi::sendResponse()line 64 flushes the 207status line and headers, then line 81 invokes the closure.
ObjectTree::getNodeForPath()throwsSabre\DAV\Exception\NotFoundas it always has, and the catch block inOCA\DAV\Connector\Sabre\Server::start()dutifully sets the status to 404 and re-sends —into a response whose headers left the building one stack frame ago.
So the 404 logic is fine. It just runs after the status code has already been committed to
the wire. The body that comes back is a degenerate multistatus:
writeMultiStatus()gets asfar as the XML prolog and the opening
<d:multistatus>, then the first loop iterationthrows,
$w->flush()never runs, and thed:errordocument from the exception handler isappended on top. That is why the responses are a uniform ~1150–1350 bytes regardless of
which non-existent path you ask for.
Affected endpoints — everything routed through
ServerFactory:/remote.php/webdavapps/dav/appinfo/v1/webdav.php/remote.php/filesremote.php:87)/public.php/webdavapps/dav/appinfo/v1/publicwebdav.php/public.php/davapps/dav/appinfo/v2/publicremote.php/remote.php/davapps/dav/appinfo/v2/remote.php→OCA\DAV\Server/remote.php/davnever touches the flag, which is exactly why the modern endpoint stillbehaves. That asymmetry is the whole tell.
I also checked whether any plugin on the legacy server happens to resolve the node during
beforeMethod:PROPFINDand would incidentally save us. None does:LockPluginreturnsearly unless the method is
PUT,ViewOnlyPluginonly hooksGET/COPY/MOVE, andFilesPluginhookspropFind, which fires inside the generator and is therefore also toolate. Nothing catches this.
Version status:
streamMultiStatuspresentmaster,stable33,stable34(checked at time of writing)Reproduced live on 33.0.7.1. I then pulled the 34.0.3 release tarball and audited it
line-by-line specifically to find out whether upgrading would save me. It would not.
Steps to reproduce
You need an app password — SAML/SSO accounts can't do basic auth against WebDAV.
PROPFINDa path that definitively does not exist, on both endpoints:Observed:
https://cloud.example.com/remote.php/webdav, then right-click → New → Folder.The folder is never created; Explorer shows "Access denied". The access log shows a burst
of
PROPFIND … 207forNeuer Ordner/New folderanddesktop.ini, and noMKCOLis ever sent, because Explorer is still waiting for a404that will never come:For comparison, the same operation against
/remote.php/dav/files/<uid>/on the same server,same minute, working exactly as intended:
"PROPFIND /remote.php/webdav. On 33.0.6 you get a steady stream of404s every singleday. From the moment 33.0.7 is installed you get exactly zero, forever. In my case
that was ~265 000 legacy PROPFINDs across twelve days without a single
404. The cutoveris visible to the second in the log, matched against the updater timestamp.
Expected behavior
PROPFINDon a non-existent path returns404 Not Foundon the legacy endpoint, the sameway it does on
/remote.php/davand the same way it did in 33.0.6 and every release beforeit.
207 Multi-Statusshould mean the resource exists and here are its properties, not"I already sent the headers, sorry".
Concretely: the node must be resolved before the status code is committed. The streaming
optimisation is fine in principle, it just needs to prime the generator first. Priming with
a single
->current()on the iterator beforesetStatus(207)runs the generator up to itsfirst
yield, which triggersgetNodeForPath()and letsNotFoundpropagate while thestatus is still mutable — and a subsequent
foreachstill works, since the generator has notadvanced past the first yield. That preserves the perf win for the normal case.
Failing that: make the flag opt-in via a config value, or revert the line on the stable
branches until it can be done safely. Silently converting every 404 into a 207 on an endpoint
that Windows depends on is not a great trade for saving some memory on PROPFIND.
Nextcloud Server version
34
Operating system
Debian/Ubuntu
PHP engine version
PHP 8.3
Web server
Apache (supported)
Database engine version
MariaDB
Is this bug present after an update or on a fresh install?
Fresh Nextcloud Server install
Are you using the Nextcloud Server Encryption module?
Encryption is Disabled
What user-backends are you using?
Configuration report
{ "system": { "instanceid": "***REMOVED SENSITIVE VALUE***", "passwordsalt": "***REMOVED SENSITIVE VALUE***", "secret": "***REMOVED SENSITIVE VALUE***", "trusted_domains": [ "domain.cloud.de", ], "trusted_proxies": "***REMOVED SENSITIVE VALUE***", "datadirectory": "***REMOVED SENSITIVE VALUE***", "overwrite.cli.url": "https:\/\/domain.cloud.de", "overwriteprotocol": "https", "dbtype": "mysql", "version": "33.0.7.1", "dbname": "***REMOVED SENSITIVE VALUE***", "dbhost": "***REMOVED SENSITIVE VALUE***", "dbport": "3306", "dbtableprefix": "oc_", "dbuser": "***REMOVED SENSITIVE VALUE***", "dbpassword": "***REMOVED SENSITIVE VALUE***", "installed": true, "appstore.experimental.enabled": true, "loglevel": 3, "log_rotate_size": 52428800, "updater.release.channel": "stable", "skeletondirectory": "", "maintenance": false, "memcache.local": "\\OC\\Memcache\\APCu", "mail_smtpmode": "smtp", "mail_smtpauthtype": "LOGIN", "mail_from_address": "***REMOVED SENSITIVE VALUE***", "mail_domain": "***REMOVED SENSITIVE VALUE***", "mail_smtphost": "***REMOVED SENSITIVE VALUE***", "mail_smtpport": "25", "ldapIgnoreNamingRules": false, "ldapProviderFactory": "\\OCA\\User_LDAP\\LDAPProviderFactory", "auth.bruteforce.protection.enabled": false, "trashbin_retention_obligation": "30,90", "mysql.utf8mb4": true, "activitiy_expire_days": 90, "mail_sendmailmode": "smtp", "default_phone_region": "DE", "filelocking.enabled": true, "memcache.distributed": "\\OC\\Memcache\\Redis", "memcache.locking": "\\OC\\Memcache\\Redis", "redis": { "host": "***REMOVED SENSITIVE VALUE***", "port": 6379, "timeout": 0, "password": "***REMOVED SENSITIVE VALUE***" }, "maintenance_window_start": 1 } } Relevant bits from mine, redacted: Nextcloud 33.0.7.1 (versionstring 33.0.7), dav app 1.36.0 also verified against 34.0.3 (dav 1.40.0) by source audit PHP 8.2.33 (NTS) Web server Apache 2, TLS terminated locally, no reverse proxy in front Database MariaDB Memcache.local \OC\Memcache\APCu Memcache.distributed / locking \OC\Memcache\Redis User backend SAML (user_saml) plus local accounts No reverse proxy, no WAF, no CDN — the web server is the edge, so nothing external is rewriting status codes. Ruled that out early.List of activated Apps
Nextcloud Signing status
Nextcloud Logs
Additional info
diffing
ServerFactory.phpacross release tags.ab34c4c21884"perf(dav): Stream PROPFIND output", backport PR [stable33] perf(dav): Stream PROPFIND output #62225into stable33. Still present on
master,stable33andstable34.404to test for existence before writing.Microsoft-WebDAV-MiniRedir(Windows mapped drives) is the obvious one and the one thathurts, since it can no longer create folders or files at all. Anything doing
PROPFIND-then-PUTagainst the legacy endpoint is presumably equally broken./remote.php/dav.Which is probably why this got through CI and a full point release.
403countsalso collapse to zero at the same upgrade. That part is not this bug — most of those
403s were forbidden-filename blocking ofdesktop.ini, and that behaviour was changeddeliberately; those requests now correctly return
404on the modern endpoint. "The 403scame back" is not a fix signal for this issue. The only reliable signal is
404returningto the legacy endpoint.
https://cloud.example.com/remote.php/webdavtohttps://cloud.example.com/remote.php/dav/files/<uid>/. Note that<uid>is the internaluser ID, which for SSO accounts may be a GUID rather than the login name — get it with
occ user:info <login>. Alternatively, delete the one line atServerFactory.php:83locally, at the cost of the streaming optimisation and your integrity check.