{"record":{"id":"c13741806e88a8aa","repo":"nextcloud/server","slug":"the-hash-headers-must-not-be-null","errorCode":null,"errorMessage":"The hash headers must not be null.","messagePattern":"The hash headers must not be null\\.","errorType":"exception","errorClass":"Sabre\\DAV\\Exception\\BadRequest","httpStatus":400,"severity":"error","filePath":"apps/dav/lib/BulkUpload/MultipartRequestParser.php","lineNumber":198,"sourceCode":"\t\t\t\t$this->logger->error('Header missing \":\" on bulk request: ' . json_encode($line));\n\t\t\t\tthrow new Exception('An error occurred while reading headers of a part', Http::STATUS_BAD_REQUEST);\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\t[$key, $value] = explode(':', $line, 2);\n\t\t\t\t$headers[strtolower(trim($key))] = trim($value);\n\t\t\t} catch (\\Exception $e) {\n\t\t\t\tthrow new BadRequest('An error occurred while parsing headers of a part', Http::STATUS_BAD_REQUEST, $e);\n\t\t\t}\n\t\t}\n\n\t\tif (!isset($headers['content-length'])) {\n\t\t\tthrow new LengthRequired('The Content-Length header must not be null.');\n\t\t}\n\n\t\t// TODO: Drop $md5 condition when the latest desktop client that uses it is no longer supported.\n\t\tif (!isset($headers['x-file-md5']) && !isset($headers['oc-checksum'])) {\n\t\t\tthrow new BadRequest('The hash headers must not be null.');\n\t\t}\n\n\t\treturn $headers;\n\t}\n\n\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","sourceCodeStart":180,"sourceCodeEnd":216,"githubUrl":"https://github.com/nextcloud/server/blob/ecdeb153ffdf227235c9a7e2d13dbe0f9c817bc3/apps/dav/lib/BulkUpload/MultipartRequestParser.php#L180-L216","documentation":"Each bulk-upload part must carry an integrity hash: either 'X-File-MD5' (legacy desktop client) or 'OC-Checksum' in 'algorithm:value' form. If neither header is present in the part, readPartHeaders() rejects it with HTTP 400 'The hash headers must not be null.' - the endpoint never accepts unverified part content.","triggerScenarios":"POST to /dav/bulk with a part whose headers contain neither x-file-md5 nor oc-checksum; e.g. a third-party client implementing the endpoint from an outdated spec, or a payload assembler that drops these headers.","commonSituations":"Custom sync scripts and integrations hitting POST /dav/bulk for the first time; clients that assumed checksums were optional; header typos like 'X-File-MD5 ' or 'OC-Checksums'.","solutions":["Send 'X-File-MD5: <lowercase hex md5>' per part, or preferably 'OC-Checksum: SHA1:<hex>' (any hash_init() algorithm works)","Compute the digest over exactly the bytes that form the part body, before boundary and CRLF","Cross-check your part layout with the parser's unit tests or a desktop client capture"],"exampleFix":"// before\n$headers = \"X-File-Path: /a.txt\\r\\nContent-Length: 3\\r\\n\\r\\n\";\n\n// after\n$headers = \"X-File-Path: /a.txt\\r\\nContent-Length: 3\\r\\n\"\n    . \"X-File-MD5: \" . md5($content) . \"\\r\\n\\r\\n\";","handlingStrategy":"validation","validationCode":"// Build the hash header for every part, computed from the exact bytes\n$part['headers']['x-file-md5'] = hash('md5', $part['content']);\n// or: $part['headers']['oc-checksum'] = 'SHA1:' . hash('sha1', $part['content']);","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Every part needs X-File-MD5 or OC-Checksum - the endpoint has no unchecked mode","Compute the digest over the raw bytes that form the part body, after all encoding is final","Watch for header names mangled by templating (wrong case is fine, typos are not)"],"tags":["php","webdav","nextcloud","bulk-upload","multipart","checksum","integrity"],"backgroundTag":"missing-checksum-header","analyzedSha":"ecdeb153ffdf227235c9a7e2d13dbe0f9c817bc3","analyzedAt":"2026-08-17T01:36:13.386Z","schemaVersion":2},"datasetVersion":"2026-08-17T04:17:16.089Z"}