nextcloud / nextcloud/richdocuments
WOPI PutFile fails with 500 (uncaught TypeError from files_versions) leading to save failures and conflict hints
Nobody has claimed this yet.
- Dominant language
- JavaScript
- Stars
- 453
- Forks
- 147
- Avg merge
- 14h 54m
- Merged PRs (30d)
- 83
Description
Summary
On a production system (Nextcloud server 34, COOLWSD 25.04, richdocuments) saves from Collabora intermittently fail. Collabora reports a conflict on the file and then fails to save. Investigation shows two distinct but related symptoms:
- Save failure (HTTP 500): A
TypeErrorthrown inside thefiles_versionswrite hook duringputContent()escapesWopiController::putFile()uncaught and returns a bare500 {"message":"Error"}to Collabora. - Conflict hint: Collabora's own timestamp-based conflict detection raises
documentconflict— partly as a downstream effect of the failed/retried saves, partly via the WOPI timestamp comparison.
Update — the 500 is a known nextcloud/server bug, already fixed on master: nextcloud/server#60842 "fix(files_versions): guard null path in event listeners" (milestone NC 35, merged 2026-06-11) guards the five callers of
getPathForNode()from passingnulldownstream — it explicitly addresses thisbasicOperation(): Argument #2 ($path) must be of type string, null givenin public-link/groupfolder scenarios. Backports are in flight, including stable34 (#61189), stable33 (#61188), stable32 (#61187), stable31 (#61329). The production version34.0.0.11predates the stable34 backport, which is why it still crashes. (An earlier, related but different null fix was #51609, NC 32.) The richdocuments-side hardening below is still worthwhile so a hook\Errorno longer surfaces as an uncaught 500.
Logs
Nextcloud (richdocuments), on POST …/wopi/files/12049115_…/contents:
Uncaught error: OC\Files\View::basicOperation(): Argument #2 ($path) must be of type string, null given,
called in …/View.php on line 525 in file …/View.php line 1204
Previous (root) exception:
TypeError: OC\Files\View::basicOperation(): Argument #2 ($path) must be of type string, null given
#0 …/View.php(525): OC\Files\View->basicOperation()
#1 …/Filesystem.php(496): OC\Files\View->file_exists()
#2 …/apps/files_versions/lib/Storage.php(165): OC\Files\Filesystem::file_exists()
#3 …/apps/files_versions/lib/Listener/FileEventsListener.php(210): OCA\Files_Versions\Storage::store()
#4 …/apps/files_versions/lib/Listener/FileEventsListener.php(83): …->write_hook()
…
#15 …/apps/richdocuments/lib/Controller/WopiController.php(665): OC\Files\Node\File->putContent()
#16 …/apps/richdocuments/lib/Controller/WopiController.php(935): {closure …putFile():665}()
#17 …/apps/richdocuments/lib/Controller/WopiController.php(912): …->retryOperation()
#18 …/lib/private/Files/Lock/LockManager.php(73): {closure …wrappedFilesystemOperation():911}()
#19 …/apps/richdocuments/lib/Controller/WopiController.php(915): …->runInScope()
#20 …/apps/richdocuments/lib/Controller/WopiController.php(665): …->wrappedFilesystemOperation()
#21 …/lib/private/AppFramework/Http/Dispatcher.php(165): …->putFile()
COOLWSD (same docKey, note the two access tokens):
WOPI::PutFile … access_token=wrsr… : 500 (Internal Server Error) Internal Server Error: {"message":"Error"}
ERR Unexpected response to WOPI::PutFile. Cannot upload file to WOPI storage … 500 …
ERR Failed to upload docKey … Notifying client.
WOPI::PutFile … access_token=0C6W… : 200 (OK) OK: {"LastModifiedTime":"2026-06-16T12:21:56.000000Z"}
… After failing to upload […], the size on WOPI host matches our uploaded last uploaded size: 1804864 bytes.
We will assume this is our last uploaded version and synchronize the timestamp …
The wrsr… token consistently returns 500; the 0C6W… token consistently returns 200 for the same document broker (docbroker_5f67).
Root cause of the 500
WopiController::putFile()calls$file->putContent($content)(lib/Controller/WopiController.php:663).putContentfires the filesystem pre-write hook before writing.files_versionslistens:FileEventsListener::write_hook()computes$path = $this->getPathForNode($node)and passes it toStorage::store($path). On stable34 there is no null check;getPathForNode()can returnnull(e.g. public-link / groupfolder contexts).Storage::store()then runsFilesystem::file_exists($path)atStorage.php:165.Filesystem::file_exists(null)→View::basicOperation(…, null)→TypeError.- A
TypeErrorextends\Error, not\Exception, so thecatch (\Exception $e)inputFile()(lib/Controller/WopiController.php:677) does not catch it. It escapes to the AppFramework dispatcher → generic500 {"message":"Error"}. → fixed upstream by nextcloud/server#60842.
Why it is per-token / intermittent
getPathForNode() resolves the node path against the current user, falling back to the owner. richdocuments sets setUserScope($wopi->getEditorUid()) / setFilesystemScope($wopi->getUserForFileAccess()) (lib/Controller/WopiController.php:633-634, :967-970) and fetches the node from getUserForFileAccess()'s folder. For a token where the editor context differs from the owner (guest/share/groupfolder editor — see Wopi::getUserForFileAccess(), lib/Db/Wopi.php:158), the path resolution can fail and return null. The owner's token resolves fine, which is why one token 500s and another succeeds.
The conflict hint
This is a separate Collabora mechanism (verified in CollaboraOnline/online.mirror, wsd/DocumentBroker.cpp). The user-visible conflict is handleDocumentConflict() → error: cmd=load kind=documentconflict, triggered in download() / CheckFileInfo handling when the storage LastModifiedTime differs from what Collabora recorded after its last save (and it is not mid-upload):
if (!_storageManager.getLastModifiedServerTimeString().empty() &&
!fileInfo.getLastModifiedServerTimeString().empty() &&
_storageManager.getLastModifiedServerTimeString() != fileInfo.getLastModifiedServerTimeString()) {
// … "Document has been modified behind our back. Informing all clients" → handleDocumentConflict();
}
richdocuments returns LastModifiedTime as Helper::toISO8601($file->getMTime()) in both CheckFileInfo (lib/Controller/WopiController.php:196) and the PutFile response (:673). Reasons the timestamp can mismatch and trigger a conflict:
- The save failures themselves: the failing token's content is never written (it throws in the pre-write hook), while the retry on the other token succeeds and bumps mtime. The failing session then sees a changed mtime → conflict.
- richdocuments' own guard:
putFile()returns409 COOLStatusCode=1010(COOL_STATUS_DOC_CHANGED) whenX-COOL-WOPI-Timestamp≠toISO8601(getMTime())(lib/Controller/WopiController.php:645-651). - Second-precision timestamps:
toISO8601()builds from integer seconds and always formats.000000(lib/Helper.php:64-69, see its ownTODO: Be more precise…). Sub-second/1-second drift is enough for a string mismatch. - Another writer (other editor/session, sync client, server-side rewrite) or object/S3 storage mtime behavior changing mtime between save and CheckFileInfo.
Suggested fixes
- Primary (nextcloud/server): done on master via #60842; ensure the stable34 backport (#61189) lands so deployments on 34.x get it. This is the real fix for the 500. -> https://github.com/nextcloud/server/pull/61189 as backport
- richdocuments hardening: widen the catch in
WopiController::putFile()to\Throwableso an\Errorfrom a hook is handled and logged with context instead of escaping as an uncaught 500. (Does not make the failing save succeed, but avoids the bare 500 and improves diagnostics.) - Optional: revisit
Helper::toISO8601()second-only precision to reduce spurious timestamp-mismatch conflicts.
Environment
- Nextcloud server: 34.0.0.11
- COOLWSD: 25.04.8.3
- richdocuments: as deployed on cloud.nextcloud.com
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with lib/Controller/WopiController.php, especially putFile() around lines 633-677 and the related scope handling around lines 967-970. Review how hook failures are handled and use the reported files_versions stack trace as context. Done means the richdocuments-side failure is handled and logged rather than escaping as a bare 500, while the upstream server backport remains separate.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- php
- Domain
- api, backend
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 55/100