Skip to content

fix(wopi): harden file saving against failures wrongly seen as conflicts#5771

Open
juliusknorr wants to merge 1 commit into
mainfrom
claude/relaxed-euler-j3p2z1
Open

fix(wopi): harden file saving against failures wrongly seen as conflicts#5771
juliusknorr wants to merge 1 commit into
mainfrom
claude/relaxed-euler-j3p2z1

Conversation

@juliusknorr

Copy link
Copy Markdown
Member

Summary

Hardens the WOPI save paths so that a failed save can no longer surface to the Collabora client as a spurious document conflict, and so that errors raised inside filesystem write hooks no longer escape as uncaught errors.

Background and full investigation: #5770.

On a production system (NC 34) saves intermittently failed with 500 Internal Server Error and the client reported a conflict. The root cause of the 500 is a TypeError thrown inside the files_versions write hook (getPathForNode() returns nullStorage::store(null)View::basicOperation(…, null)). That is a nextcloud/server bug already fixed on master in nextcloud/server#60842 (backport to stable34: #61189). Because a TypeError is an \Error and not an \Exception, our catch (\Exception) did not catch it, so it escaped as an opaque 500 — which the client can then reconcile into a false conflict.

Changes

  • Catch \Throwable (not just \Exception) in putFile() and postFile(), so errors raised inside filesystem hooks produce a controlled response and a meaningful log entry instead of an uncaught error.
  • mtime no-op guard: snapshot the file's mtime before writing and log a distinct error if it changed despite a failed save. An advanced mtime after a failure indicates a partial write, which is exactly what makes the client assume the document changed externally.
  • LockedException409 Conflict instead of 500, so the client retries the save instead of treating a transient lock as a server error.

This does not (and should not) suppress conflicts caused by a genuinely concurrent successful save — it only ensures a failed save never looks like an external change.

Follow-up (not in this PR)

  • Revisit Helper::toISO8601() second-only precision (always .000000) to reduce timestamp-rounding false positives — needs validation against the Collabora client and is tracked separately.

Testing

  • php -l clean. Manual reasoning against the reported production traces; no automated test added for the hook-error path as it requires the versions listener fault injection.

AI tool use disclosure

This change was prepared with the assistance of an AI coding agent (Claude Code). The investigation, code changes, and this description were AI-assisted and reviewed by the committer before submission. Commits carry an Assisted-by: trailer.

https://claude.ai/code/session_0148r9TcHSywDqJVHqyMggub


Generated by Claude Code

Errors raised inside filesystem write hooks (for example a TypeError from
the files_versions listener when a node path cannot be resolved) previously
escaped putFile()/postFile() uncaught, because the catch blocks only handled
\Exception while a TypeError is an \Error. The request then returned an
opaque 500 to the WOPI client, which can in turn surface as a spurious
document conflict.

- Catch \Throwable (not just \Exception) so hook errors produce a
  controlled response and a meaningful log entry instead of an uncaught error.
- Snapshot the mtime before writing and log a distinct error if it changed
  despite a failed save (a partial write), since an advanced mtime makes the
  client assume the document changed externally.
- Return 409 Conflict instead of 500 for LockedException so the client
  retries the save instead of treating the lock as a server error.

Signed-off-by: Julius Knorr <jus@bitgrid.net>
Assisted-by: Claude Code
$this->logger->warning($e->getMessage(), ['exception' => $e]);
return new JSONResponse([], Http::STATUS_NOT_FOUND);
} catch (\Exception $e) {
} catch (\Throwable $e) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not a fan of catching a Throwable, the fix in server is almost fully backported

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants