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
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 TypeError thrown inside the files_versions write hook during putContent() escapes WopiController::putFile() uncaught and returns a bare 500 {"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 passing null downstream — it explicitly addresses this basicOperation(): Argument #2 ($path) must be of type string, null given in public-link/groupfolder scenarios. Backports are in flight, including stable34 (#61189), stable33 (#61188), stable32 (#61187), stable31 (#61329). The production version 34.0.0.11 predates 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 \Error no 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
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).
putContent fires the filesystem pre-write hook before writing.
files_versions listens: FileEventsListener::write_hook() computes $path = $this->getPathForNode($node) and passes it to Storage::store($path). On stable34 there is no null check; getPathForNode() can return null (e.g. public-link / groupfolder contexts). Storage::store() then runs Filesystem::file_exists($path) at Storage.php:165.
A TypeError extends \Error, not\Exception, so the catch (\Exception $e) in putFile() (lib/Controller/WopiController.php:677) does not catch it. It escapes to the AppFramework dispatcher → generic 500 {"message":"Error"}. → fixed upstream by fix(files_versions): guard null path in event listeners 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() returns 409 COOLStatusCode=1010 (COOL_STATUS_DOC_CHANGED) when X-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 own TODO: 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.
richdocuments hardening: widen the catch in WopiController::putFile() to \Throwable so an \Error from 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.
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:
TypeErrorthrown inside thefiles_versionswrite hook duringputContent()escapesWopiController::putFile()uncaught and returns a bare500 {"message":"Error"}to Collabora.documentconflict— partly as a downstream effect of the failed/retried saves, partly via the WOPI timestamp comparison.Logs
Nextcloud (
richdocuments), onPOST …/wopi/files/12049115_…/contents:Previous (root) exception:
COOLWSD (same docKey, note the two access tokens):
The
wrsr…token consistently returns 500; the0C6W…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.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 fix(files_versions): guard null path in event listeners server#60842.Why it is per-token / intermittent
getPathForNode()resolves the node path against the current user, falling back to the owner. richdocuments setssetUserScope($wopi->getEditorUid())/setFilesystemScope($wopi->getUserForFileAccess())(lib/Controller/WopiController.php:633-634,:967-970) and fetches the node fromgetUserForFileAccess()'s folder. For a token where the editor context differs from the owner (guest/share/groupfolder editor — seeWopi::getUserForFileAccess(),lib/Db/Wopi.php:158), the path resolution can fail and returnnull. 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 ishandleDocumentConflict()→error: cmd=load kind=documentconflict, triggered indownload()/ CheckFileInfo handling when the storageLastModifiedTimediffers from what Collabora recorded after its last save (and it is not mid-upload):richdocuments returns
LastModifiedTimeasHelper::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:putFile()returns409 COOLStatusCode=1010(COOL_STATUS_DOC_CHANGED) whenX-COOL-WOPI-Timestamp≠toISO8601(getMTime())(lib/Controller/WopiController.php:645-651).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.Suggested fixes
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.)Helper::toISO8601()second-only precision to reduce spurious timestamp-mismatch conflicts.Environment