{"record":{"id":"14a55f25b5aabe71","repo":"nextcloud/server","slug":"vcard-can-not-be-empty","errorCode":null,"errorMessage":"vCard can not be empty","messagePattern":"vCard can not be empty","errorType":"exception","errorClass":"BadRequest","httpStatus":400,"severity":"error","filePath":"apps/dav/lib/CardDAV/CardDavBackend.php","lineNumber":1583,"sourceCode":"\t/**\n\t * Extract UID from vcard\n\t *\n\t * @param string $cardData the vcard raw data\n\t * @return string the uid\n\t * @throws BadRequest if no UID is available or vcard is empty\n\t */\n\tprivate function getUID(string $cardData): string {\n\t\tif ($cardData !== '') {\n\t\t\t$vCard = Reader::read($cardData);\n\t\t\tif ($vCard->UID) {\n\t\t\t\t$uid = $vCard->UID->getValue();\n\t\t\t\treturn $uid;\n\t\t\t}\n\t\t\t// should already be handled, but just in case\n\t\t\tthrow new BadRequest('vCards on CardDAV servers MUST have a UID property');\n\t\t}\n\t\t// should already be handled, but just in case\n\t\tthrow new BadRequest('vCard can not be empty');\n\t}\n\n\t/**\n\t * Mark all cards in an address book as needing to be validated\n\t *\n\t * This is done by setting the modified date to `null`, once a sync runs\n\t * the mtime will be set to a non-null value. Leaving all deleted items with\n\t * a null modified date.\n\t */\n\tpublic function markCardsAsPending(int $addressBookId): void {\n\t\t$query = $this->db->getTypedQueryBuilder();\n\t\t$query->update($this->dbCardsTable)\n\t\t\t->set('lastmodified', $query->createNamedParameter(null))\n\t\t\t->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)))\n\t\t\t->executeStatement();\n\t}\n\n\t/**","sourceCodeStart":1565,"sourceCodeEnd":1601,"githubUrl":"https://github.com/nextcloud/server/blob/ecdeb153ffdf227235c9a7e2d13dbe0f9c817bc3/apps/dav/lib/CardDAV/CardDavBackend.php#L1565-L1601","documentation":"getUID()'s empty branch throws BadRequest('vCard can not be empty') when $cardData is the empty string - a backstop guard (the comment notes it 'should already be handled') against card creation or update calls with an empty payload. It surfaces as HTTP 400 on the DAV request.","triggerScenarios":"PUT to a card URL with an empty request body; upstream code truncating carddata to '' before createCard()/updateCard(); server-side callers passing an uninitialized string.","commonSituations":"Client bugs uploading a zero-byte file; import pipelines writing the card before reading it; a 'clear card' feature implemented as PUT '' instead of DELETE.","solutions":["Validate that carddata is non-empty (after trim) before calling the backend or issuing PUT","Use DELETE to remove a card, never an empty PUT","Log the body length at the client to find where truncation happens"],"exampleFix":"// before\n$backend->createCard($bookId, $uri, ''); // 400 vCard can not be empty\n\n// after\nif (trim((string) $cardData) === '') {\n    throw new InvalidArgumentException('refusing to store an empty vCard');\n}\n$backend->createCard($bookId, $uri, $cardData);","handlingStrategy":"validation","validationCode":"if (trim((string) $cardData) === '') {\n    throw new InvalidArgumentException('refusing to PUT an empty vCard');\n}","typeGuard":"function isNonEmptyCardData(?string $cardData): bool\n{\n    return $cardData !== null && trim($cardData) !== '';\n}","tryCatchPattern":"try {\n    $backend->createCard($bookId, $uri, $cardData);\n} catch (\\Sabre\\DAV\\Exception\\BadRequest $e) {\n    if (str_contains($e->getMessage(), 'can not be empty')) {\n        // fix the upstream truncation instead of retrying with ''\n    }\n}","preventionTips":["Use DELETE to remove cards, never an empty PUT","Assert a non-empty payload after every transport or transformation step","Log request body lengths client-side when debugging truncated uploads"],"tags":["carddav","vcard","http-400","validation","empty-body"],"backgroundTag":"empty-request-body","analyzedSha":"ecdeb153ffdf227235c9a7e2d13dbe0f9c817bc3","analyzedAt":"2026-08-17T01:36:13.386Z","schemaVersion":2},"datasetVersion":"2026-08-17T04:17:16.089Z"}