nextcloud / nextcloud/richdocuments

WOPI PutFile fails with 500 (uncaught TypeError from files_versions) leading to save failures and conflict hints

Open
#5,770 1 comment 0 reactions 0 assignees View on GitHub

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:

  1. 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.
  2. 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

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

  1. WopiController::putFile() calls $file->putContent($content) (lib/Controller/WopiController.php:663).
  2. putContent fires the filesystem pre-write hook before writing.
  3. 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.
  4. Filesystem::file_exists(null)View::basicOperation(…, null)TypeError.
  5. 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 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() returns 409 COOLStatusCode=1010 (COOL_STATUS_DOC_CHANGED) when X-COOL-WOPI-TimestamptoISO8601(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.

Suggested fixes

  1. 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
  2. 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.)
  3. 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.