Hi,
I believe I have identified a false document-conflict condition when using Collabora Online 26.04 with Microsoft SharePoint as the native WOPI host.
Environment
Collabora Online: 26.04.3.1
WOPI host: Microsoft SharePoint Server
WOPI endpoint: native /_vti_bin/wopi.ashx implementation
Editing and WOPI locking otherwise work.
The problem occurs intermittently after a successful save. Users can receive a “Document changed in storage” / timestamp mismatch even though the apparent external modification is actually Collabora’s own preceding PutFile.
A representative sequence from the COOLWSD log is:
09:18:07
Invalid or missing JSON in WOPI::PutFile HTTP_OK response.
Expected json object with a LastModifiedTime value
09:18:09
Document [...] has been modified behind our back.
Expected: 2026-08-17T12:32:45.0000000Z
Actual: 2026-08-21T09:18:07.0000000Z
The significant detail is that SharePoint’s new LastModifiedTime is 09:18:07, exactly the second in which Collabora performed the successful PutFile.
Looking at the current distro/collabora/co-26.04 source, I think the sequence is:
Collabora knows timestamp T0
|
v
PutFile to SharePoint
|
v
SharePoint successfully saves the file and changes timestamp to T1
|
v
SharePoint returns HTTP 200 without a JSON LastModifiedTime
|
v
WopiStorage logs:
"Invalid or missing JSON ... Expected ... LastModifiedTime"
|
v
The old T0 remains cached in WopiStorage
|
v
DocumentBroker::handleUploadToStorageSuccessful()
copies _storage->getLastModifiedTime() into StorageManager
|
v
Next CheckFileInfo returns T1
|
v
T0 != T1
|
v
"Document has been modified behind our back"
The relevant code appears to be in wsd/wopi/WopiStorage.cpp, in WopiStorage::handleUploadToStorageResponse().
For HTTP 200, the code tries to parse the response as JSON and extract LastModifiedTime. However, in the branch where the HTTP 200 response is not valid JSON, it only logs the warning. It does not invalidate the timestamp that existed before the PutFile.
This seems problematic for Microsoft SharePoint because Microsoft’s WOPI PutFile specification defines 200 OK as success and does not require a JSON response body containing LastModifiedTime. X-WOPI-ItemVersion is an optional response header. In contrast, Collabora’s external-change extension expects LastModifiedTime to be available from PutFile.
Would it therefore make sense for the HTTP-200/no-LastModifiedTime case to invalidate the old cached timestamp?
For example, conceptually:
if (details.httpResponseCode == http::StatusCode::OK)
{
result.setResult(StorageBase::UploadResult::Result::OK);
if (valid JSON containing LastModifiedTime)
{
setLastModifiedTime(lastModifiedTime);
}
else
{
// PutFile succeeded, but the upload itself may have
// changed LastModifiedTime in storage. The timestamp
// from before PutFile is no longer trustworthy.
setLastModifiedTimeUnSafe();
}
}
Then DocumentBroker could reuse its existing DocumentState::Activity::SyncFileTimestamp mechanism to perform CheckFileInfo and establish the post-upload timestamp.
There is already very similar recovery logic for the case where a PutFile fails/times out and Collabora cannot know whether the WOPI host actually committed the upload. It seems that HTTP 200 + no post-upload LastModifiedTime has a similar timestamp state: the upload itself is known to have succeeded, but its resulting storage timestamp is unknown.
I would propose:
PutFile -> HTTP 200 + LastModifiedTime
-> existing behaviour
PutFile -> HTTP 200 without usable LastModifiedTime
-> mark cached LastModifiedTime unsafe
-> successful save remains successful
-> run CheckFileInfo / SyncFileTimestamp
-> adopt returned timestamp as the new baseline
This should avoid the false conflict without disabling external-change detection. A genuine later modification would still change the timestamp from the newly synchronized value and trigger the normal conflict logic.
As a fallback, if the document closes before CheckFileInfo can be executed, the next CheckFileInfo should establish the first non-empty timestamp as the new baseline rather than comparing it with the now-invalid pre-upload value.
While reviewing the existing SyncFileTimestamp code I also noticed this condition:
if (_storageManager.getSizeAsUploaded() == size ||
_storageManager.getSizeOnServer())
getSizeOnServer() returns std::size_t, so unless I am missing something, I think the second part should be:
_storageManager.getSizeOnServer() == size
Otherwise any non-zero previous server size makes the whole condition true.
Could someone from the Collabora team confirm:
- Is a JSON
LastModifiedTimein a successful PutFile response intentionally considered mandatory for SharePoint integration? - Would marking the old timestamp unsafe and invoking the existing
SyncFileTimestamppath be the preferred fix for a standards-compliant200 OKwithout that JSON value? - Is there perhaps already a SharePoint-related patch/hotfix for this in a newer/private branch?
- Does the
getSizeOnServer()condition above indeed look like a missing== sizecomparison?
I can provide additional sanitized COOLWSD logs if useful.
Thanks.