{"record":{"id":"2a9760a7ae7e30c6","repo":"navidrome/navidrome","slug":"forbidden-2a9760","errorCode":null,"errorMessage":"Forbidden","messagePattern":"Forbidden","errorType":"http","errorClass":null,"httpStatus":403,"severity":"warning","filePath":"server/jellyfin/playlists.go","lineNumber":37,"sourceCode":"\n// playlistsFolder is the item returned for a ManualPlaylistsFolder query. CollectionType must be\n// \"playlists\" — how the client identifies it; without it Jellify's playlist-library query loops.\nfunc playlistsFolder() dto.BaseItemDto {\n\treturn dto.BaseItemDto{\n\t\tId:             dto.PlaylistsFolderGUID,\n\t\tName:           \"Playlists\",\n\t\tType:           \"ManualPlaylistsFolder\",\n\t\tCollectionType: \"playlists\",\n\t\tIsFolder:       true,\n\t}\n}\n\n// playlistError maps core/playlists write errors to HTTP status: ownership or locked -> 403,\n// missing/invisible -> 404 (never revealing another user's private playlist), else -> 500.\nfunc (api *Router) playlistError(w http.ResponseWriter, r *http.Request, err error) {\n\tswitch {\n\tcase errors.Is(err, model.ErrNotAuthorized), errors.Is(err, model.ErrPlaylistNotEditable):\n\t\thttp.Error(w, \"Forbidden\", http.StatusForbidden)\n\tcase errors.Is(err, model.ErrNotFound):\n\t\thttp.Error(w, \"Not Found\", http.StatusNotFound)\n\tdefault:\n\t\tapi.internalError(w, r, err)\n\t}\n}\n\ntype createPlaylistRequest struct {\n\tName      string   `json:\"Name\"`\n\tIds       []string `json:\"Ids\"`\n\tMediaType string   `json:\"MediaType\"`\n}\n\n// createPlaylist always creates a new playlist (playlistId \"\" tells core/playlists.Create not to\n// replace an existing one), owned by the authenticated user.\nfunc (api *Router) createPlaylist(w http.ResponseWriter, r *http.Request) {\n\tvar body createPlaylistRequest\n\tif err := json.NewDecoder(r.Body).Decode(&body); err != nil {","sourceCodeStart":19,"sourceCodeEnd":55,"githubUrl":"https://github.com/navidrome/navidrome/blob/4ed7494a3293a9e9e647897ebfb9be327efd981b/server/jellyfin/playlists.go#L19-L55","documentation":"playlistError (server/jellyfin/playlists.go) maps core/playlists write errors to HTTP statuses for mutation endpoints (used by deleteItem and updatePlaylist, i.e. DELETE/POST on playlists). model.ErrNotAuthorized and model.ErrPlaylistNotEditable become 403 'Forbidden' — the requester is authenticated but does not own the playlist, or the playlist is locked/read-only (e.g. a smart/auto playlist or one imported read-only). Per the comment, a missing or other-user-invisible playlist would be 404 instead, so a 403 means the playlist exists and you can see it, but you may not modify it.","triggerScenarios":"DELETE /Items/{playlistId} or playlist-update calls on a playlist owned by another user; attempting to modify a locked or smart playlist; an admin-imported playlist that is read-only for regular users; concurrent ownership changes after the client cached the playlist.","commonSituations":"Shared/family servers where each user only edits their own playlists; trying to edit smart playlists generated from rules instead of manual track lists; scripts running as a service account trying to clean up other users' playlists.","solutions":["Authenticate as the user who owns the playlist, or create your own copy and edit that","If the playlist is locked/smart, change it via the mechanism that manages it (rules editor or source sync) rather than direct item mutation","Use the admin account if administrative playlist management is required","Check which mutation failed: DELETE vs update, and confirm the playlist type supports that operation"],"exampleFix":"// before: editing another user's playlist as service account -> 403\nadminClient.Delete(\"/Items/\" + userPlaylistID)\n// after: act as the owner or clone it\nuserClient := NewClient(tokenFor(playlistOwner))\nuserClient.Delete(\"/Items/\" + playlistID)","handlingStrategy":"fallback","validationCode":null,"typeGuard":null,"tryCatchPattern":"resp, err := client.Delete(\"/Items/\" + playlistID)\nif err == nil && resp.StatusCode == http.StatusForbidden {\n    // not owner or locked playlist: clone under own account, then edit the copy\n    newID := client.ClonePlaylist(playlistID)\n    return client.Delete(\"/Items/\" + newID)\n}","preventionTips":["Only mutate playlists owned by the authenticated user","Treat smart/locked playlists as read-only; change the underlying rules instead","Use an admin account for cross-user playlist maintenance"],"tags":["http-403","jellyfin","authorization","playlists"],"backgroundTag":"insufficient-permissions","analyzedSha":"4ed7494a3293a9e9e647897ebfb9be327efd981b","analyzedAt":"2026-09-01T05:03:05.018Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T05:18:18.240Z"}