fix(dav): only derive the write size from a PUT Content-Length - #63510
Open
mosi-kha wants to merge 1 commit into
Open
fix(dav): only derive the write size from a PUT Content-Length#63510mosi-kha wants to merge 1 commit into
mosi-kha wants to merge 1 commit into
Conversation
put() is not only reached by PUT. The chunked upload assembly step is a MOVE or COPY of <upload>/.file: Directory::moveInto() and ::copyInto() only short-circuit File and Directory sources at storage level, so a FutureFile falls through to Tree::copyNode() -> createFile() -> put(), where the data is the AssemblyStream of the uploaded chunks and the request itself carries no body at all. For those requests the Content-Length says nothing about the stream being written. Clients that send "Content-Length: 0" there, Safari among them, made File::put() hand a 0 to IWriteStreamStorage::writeStream(). Storages that only measure the stream when they are given no size, ObjectStoreStorage among them, then wrote an empty file while the request still answered 201, so the upload looked successful and the file was silently empty. The size comparison further down already restricts itself to PUT for exactly this reason. Apply the same restriction when deriving the expected size, which covers both the MOVE and the COPY variant. Genuine empty PUT uploads are unaffected: the method is still PUT, so a Content-Length of 0 is passed through as before. Signed-off-by: mostafa <mostafakhaki00@gmail.com> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
mosi-kha
requested review from
Altahrim,
icewind1991,
provokateurin and
salmart-dev
and removed request for
a team
August 23, 2026 15:38
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
MOVEwithContent-Length: 0results in 0bytes files in Object Storage #63504Summary
File::put()takes the expected write size from the request'sContent-Length, but it is not reached only byPUT. The chunked upload assembly step is aMOVE(orCOPY) of<upload>/.file:Directory::moveInto()andDirectory::copyInto()only short-circuitFileandDirectorysources at storage level, so aFutureFilefalls through toTree::copyNode()→Directory::createFile()→File::put(). There the data is theAssemblyStreamof the uploaded chunks and the request itself carries no body at all, so itsContent-Lengthdescribes nothing relevant.Clients that send
Content-Length: 0on that request — Safari does, Chrome omits the header entirely — make$expected0instead ofnull.ObjectStoreStorage::writeStream()only measures the stream when it is given no size:so the
fstat()fallback is skipped,$stat['size']becomes0, and the file is recorded as empty while the request still answers201. The upload looks successful and the file is silently empty.The size comparison further down in the same method is already restricted to
PUTfor exactly this reason:This applies the same restriction when deriving the expected size in the first place. Because the guard is on the method rather than on the value, it covers the
COPYvariant as well asMOVE— a fix that special-cased a zero length, or onlyMOVE, would not.Genuine empty
PUTuploads are unaffected: the method is stillPUT, so aContent-Lengthof0is passed through exactly as before.Scope
Object-storage primary storage is affected.
OC\Files\Storage\Local::writeStream()ignores the$sizeargument entirely and just callsfile_put_contents(), so local-storage installs should not be.Methods that reach
File::put():put()Content-LengthmeaningfulPUTCorePlugin::httpPutMOVEof<upload>/.fileAssemblyStreamCOPYof<upload>/.fileAssemblyStreamMOVE/COPYof a normal fileTesting
FileTest::testPutExpectedSizeOnlyComesFromPutContentLengthcovers the size handed towriteStream()forPUT(with a length, with0, and with the header absent) and forMOVE/COPY(with0and with a non-zero length).Against
masterwithout the change, 4 of the 7 cases fail:With the change,
Tests: 7, Assertions: 21, all passing.Verified end to end as well, on 33.0.7 with nginx + php-fpm and S3 (MinIO) primary storage, using a 102 395 904 byte file in five chunks. Adding
Content-Length: 0to the assembly request is the only variable that breaks it, for bothMOVEandCOPY, sequentially or in parallel.Optional hardening, not included here
@vpecinka independently diagnosed this in #7995 (comment) and patched the storage layer instead, forcing
$size = nullinObjectStoreStorage::writeStream()when a zero is announced for a stream thatfstat()shows is larger. That defends against any other caller passing a bogus zero, which this change does not.It is deliberately left out to keep this PR to the root cause;
writeStream()putting the announced size straight into the filecache is arguably worth guarding on its own. Happy to add it here or as a separate PR if you would prefer it.Checklist
php-cs-fixerreports no changes for both files)3. to review, feature component)stable32)AI (if applicable)