{"record":{"id":"8530e1ced3dbf398","repo":"nextcloud/server","slug":"unexpected-eof-while-reading-stream","errorCode":null,"errorMessage":"Unexpected EOF while reading stream.","messagePattern":"Unexpected EOF while reading stream\\.","errorType":"exception","errorClass":"Sabre\\DAV\\Exception","httpStatus":500,"severity":"error","filePath":"apps/dav/lib/BulkUpload/MultipartRequestParser.php","lineNumber":222,"sourceCode":"\t/**\n\t * Return the content of a part of the multipart body.\n\t *\n\t * @throws Exception\n\t * @throws BadRequest\n\t */\n\tprivate function readPartContent(int $length): string {\n\t\tif ($length === 0) {\n\t\t\t$content = '';\n\t\t} else {\n\t\t\t$content = stream_get_line($this->stream, $length);\n\t\t}\n\n\t\tif ($content === false) {\n\t\t\tthrow new Exception(\"Fail to read part's content.\");\n\t\t}\n\n\t\tif ($length !== 0 && feof($this->stream)) {\n\t\t\tthrow new Exception('Unexpected EOF while reading stream.');\n\t\t}\n\n\t\t// Read '\\r\\n'.\n\t\tstream_get_contents($this->stream, 2);\n\n\t\treturn $content;\n\t}\n\n\t/**\n\t * Compute the MD5 or checksum hash of the next x bytes.\n\t * TODO: Drop $md5 argument when the latest desktop client that uses it is no longer supported.\n\t */\n\tprivate function validateHash(int $length, string $fileMd5Header, string $checksumHeader): void {\n\t\tif ($checksumHeader !== '') {\n\t\t\t[$algorithm, $hash] = explode(':', $checksumHeader, 2);\n\t\t} elseif ($fileMd5Header !== '') {\n\t\t\t$algorithm = 'md5';\n\t\t\t$hash = $fileMd5Header;","sourceCodeStart":204,"sourceCodeEnd":240,"githubUrl":"https://github.com/nextcloud/server/blob/ecdeb153ffdf227235c9a7e2d13dbe0f9c817bc3/apps/dav/lib/BulkUpload/MultipartRequestParser.php#L204-L240","documentation":"After reading Content-Length bytes of a part, feof() must still be false: a '\\r\\n' plus the next boundary (or the final boundary) has to follow. If the stream is already at EOF, the declared length overran the real body and parsing aborts with 'Unexpected EOF while reading stream.' (returned to the client as HTTP 400 by BulkUploadPlugin).","triggerScenarios":"A part's Content-Length is larger than the bytes actually sent for that part, or the whole request body ends immediately after the part content so the trailing '\\r\\n--boundary' never arrives.","commonSituations":"Client computes the length in characters instead of bytes (mb_strlen vs strlen on UTF-8), hashes/lengths computed before an encoding transform, truncated transfer, or off-by-N bugs in hand-rolled multipart writers.","solutions":["Set Content-Length to the exact byte count of the raw part body (strlen($content), never mb_strlen)","Dump the wire payload and verify each part's byte count against its declared Content-Length","Build the request with a maintained multipart library instead of manual concatenation"],"exampleFix":"// before\n\"Content-Length: \" . mb_strlen($content) . \"\\r\\n\" // counts code points, not bytes\n\n// after\n\"Content-Length: \" . strlen($content) . \"\\r\\n\" // exact byte length","handlingStrategy":"validation","validationCode":"// Client-side: length and hash must derive from the same final byte string\n$content = $part['content'];\n$headers = \"Content-Length: \" . strlen($content) . \"\\r\\n\"\n    . \"X-File-MD5: \" . md5($content) . \"\\r\\n\";","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Freeze the part body bytes first, then derive Content-Length and hash from that same string","Use strlen (bytes), never mb_strlen (code points)","Ensure the trailing '\\r\\n--<boundary>' after each part is actually written"],"tags":["php","webdav","nextcloud","bulk-upload","multipart","eof","content-length"],"backgroundTag":"truncated-request-body","analyzedSha":"ecdeb153ffdf227235c9a7e2d13dbe0f9c817bc3","analyzedAt":"2026-08-17T01:36:13.386Z","schemaVersion":2},"datasetVersion":"2026-08-17T04:17:16.089Z"}