A folder created with folders/AddUsingPath in a generic list has no list item — breaking its permissions returns 404
In a document library, web/folders/AddUsingPath gives you a folder and its list item. In a generic list (BaseTemplate 100) it gives you only the folder: no list item, so there is nothing to break permissions on. Create folders in lists with GetList(…)/AddValidateUpdateItemUsingPath and UnderlyingObjectType: 1, and after creating, check that the folder has a list item — Exists: true proves nothing.
V knihovně dokumentů dá web/folders/AddUsingPath složku i s položkou seznamu. V obyčejném seznamu (BaseTemplate 100) jen složku bez položky – a na ní se oprávnění nastavit nedají. Složky v seznamu zakládej přes GetList(…)/AddValidateUpdateItemUsingPath s UnderlyingObjectType: 1 a po založení ověř, že složka má položku – Exists: true nic nedokazuje.
Symptom
You keep per-record data in a custom list and give every record its own folder with unique permissions (a common way to keep rows of one person readable only by HR and that person). The same helper has worked for years in document libraries. In the list:
POST /_api/web/folders/AddUsingPath(DecodedUrl='/sites/hr/Lists/Pay/12') → 200
GET /_api/web/GetFolderByServerRelativePath(DecodedUrl='/sites/hr/Lists/Pay/12')?$select=Exists → { "Exists": true }
POST /_api/web/GetFolderByServerRelativePath(DecodedUrl='/sites/hr/Lists/Pay/12')/ListItemAllFields/breakroleinheritance(copyRoleAssignments=false,clearSubscopes=true) → 404
The folder exists, yet every permission call on it fails.
Cause
In a generic list, web/folders/AddUsingPath creates the folder in the list's file system but does not create the list item that represents it:
GET …/GetFolderByServerRelativePath(DecodedUrl='/sites/hr/Lists/Pay/12')/ListItemAllFields?$select=Id
→ { "odata.null": true }
That is the OData v3 shape (a plain fetch with odata=nometadata). SPHttpClient in SPFx sends odata-version: 4.0, and then the same call returns { "@odata.context": "…$metadata#Edm.Null", "@odata.null": true } — with an @. Code that checks only odata.null will never see the naked folder from SPFx. Check both shapes, or decide by the presence of Id.
The folder shows up in /folders but not in /items. Permissions live on the list item (ListItemAllFields/roleassignments), so with no item there is nothing to break inheritance on — hence 404. In a document library the same method creates the item, which is why the trap stays hidden until a library pattern is reused in a list.
Fix
Create list folders as list items:
POST /_api/web/GetList(@l)/AddValidateUpdateItemUsingPath?@l='%2Fsites%2Fhr%2FLists%2FPay'
Content-Type: application/json;odata=nometadata
{
"listItemCreateInfo": {
"FolderPath": { "DecodedUrl": "/sites/hr/Lists/Pay" },
"LeafName": { "DecodedUrl": "12" },
"UnderlyingObjectType": 1
},
"formValues": [ { "FieldName": "Title", "FieldValue": "12" } ],
"bNewDocumentUpdate": false
}
This works even when the list has EnableFolderCreation: false — that setting only hides the New folder button in the UI.
Then make your helper check the item state, not existence:
- After creating (or finding) the folder, read
…/ListItemAllFields?$select=Id.odata.nullor@odata.null= naked folder, anId= a real item. Only then break inheritance. Measure that response with the same client and headers your code uses — a mock built from a consolefetchhides the v4 shape. - If a naked folder already exists and is empty (
?$select=ItemCount→ 0), send it to the recycle bin (…/recycle()) and create it again as an item. If it is not empty, stop and report it — deleting a folder whose contents have no item would lose them. - Rows you add into the folder later (
AddValidateUpdateItemUsingPathwithUnderlyingObjectType: 0andFolderPathset to the folder) inherit its permissions as usual.