{"record":{"id":"114e360412ccd8d6","repo":"nextcloud/server","slug":"http-owncloud-org-ns-share-needs-at-least-one-s","errorCode":null,"errorMessage":"{http://owncloud.org/ns}share needs at least one set or remove element","messagePattern":"(.+?)share needs at least one set or remove element","errorType":"exception","errorClass":"Sabre\\DAV\\Exception\\BadRequest","httpStatus":400,"severity":"error","filePath":"apps/dav/lib/DAV/Sharing/Plugin.php","lineNumber":184,"sourceCode":"\t\t\t\t// Adding this because sending a response body may cause issues,\n\t\t\t\t// and I wanted some type of indicator the response was handled.\n\t\t\t\t$response->setHeader('X-Sabre-Status', 'everything-went-well');\n\n\t\t\t\t// Breaking the event chain\n\t\t\t\treturn false;\n\t\t}\n\t}\n\n\tprivate function validateShareRequest($shareRequest): void {\n\t\tif (!$shareRequest instanceof ShareRequest) {\n\t\t\t// @FIXME: Replace switch-case in httpPost with instanceof ShareRequest\n\t\t\tthrow new BadRequest('The given request is not valid');\n\t\t}\n\n\t\t$elements = (count($shareRequest->set) + count($shareRequest->remove));\n\n\t\tif ($elements === 0) {\n\t\t\tthrow new BadRequest(ShareRequest::ELEMENT_SHARE . ' needs at least one set or remove element');\n\t\t}\n\n\t\tif ($elements > 10) {\n\t\t\tthrow new BadRequest(ShareRequest::ELEMENT_SHARE . ' is limited to 10 set or remove elements');\n\t\t}\n\t}\n\n\tprivate function preloadCollection(PropFind $propFind, ICollection $collection): void {\n\t\tif (!$collection instanceof CalendarHome || $propFind->getDepth() !== 1) {\n\t\t\treturn;\n\t\t}\n\n\t\t$backend = $collection->getCalDAVBackend();\n\t\tif (!$backend instanceof CalDavBackend) {\n\t\t\treturn;\n\t\t}\n\n\t\t$calendars = $collection->getChildren();","sourceCodeStart":166,"sourceCodeEnd":202,"githubUrl":"https://github.com/nextcloud/server/blob/ecdeb153ffdf227235c9a7e2d13dbe0f9c817bc3/apps/dav/lib/DAV/Sharing/Plugin.php#L166-L202","documentation":"HTTP 400 BadRequest from validateShareRequest() when the deserialized ShareRequest contains zero set elements and zero remove elements (count($shareRequest->set) + count($shareRequest->remove) === 0). A share POST that changes nothing is rejected as invalid before updateShares() runs.","triggerScenarios":"POSTing an <oc:share> body with no child elements, or whose children deserialize into empty set/remove arrays — typical when 'unshare everyone' code computes the remove list dynamically and it ends up empty but still sends the POST.","commonSituations":"Diff-based sync clients that POST the result of an empty diff; UI code that always fires the share request even when the user confirmed no changes; XML with children in an unrecognized namespace so sabre maps none of them into set/remove.","solutions":["Skip the POST entirely when the computed set/remove diff is empty","Include at least one set or remove element (each with a DAV: href) in the body","When unsharing all sharees, enumerate current invitees and emit one remove element per sharee"],"exampleFix":"// before: empty operation list -> 400\n<x1:share xmlns:x1='http://owncloud.org/ns'></x1:share>\n// after: at least one set or remove element\n<x1:share xmlns:x1='http://owncloud.org/ns' xmlns:d='DAV:'>\n  <x1:remove><d:href>principal:principals/users/bob</d:href></x1:remove>\n</x1:share>","handlingStrategy":"validation","validationCode":"// never send an empty operation list\nfunction buildShareBody(setList, removeList) {\n  if (setList.length + removeList.length === 0) return null; // skip the POST\n  return shareXml({ set: setList, remove: removeList });\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Compute the set/remove diff first and short-circuit on empty","Log the outgoing body when a 400 arrives to confirm which guard fired"],"tags":["dav","sharing","xml","bad-request","empty-payload"],"backgroundTag":"invalid-request-body","analyzedSha":"ecdeb153ffdf227235c9a7e2d13dbe0f9c817bc3","analyzedAt":"2026-08-17T01:36:13.386Z","schemaVersion":2},"datasetVersion":"2026-08-17T04:17:16.089Z"}