navidrome/navidrome · warning
Forbidden
Error message
Forbidden
What it means
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.
Source
Thrown at server/jellyfin/playlists.go:37
// playlistsFolder is the item returned for a ManualPlaylistsFolder query. CollectionType must be
// "playlists" — how the client identifies it; without it Jellify's playlist-library query loops.
func playlistsFolder() dto.BaseItemDto {
return dto.BaseItemDto{
Id: dto.PlaylistsFolderGUID,
Name: "Playlists",
Type: "ManualPlaylistsFolder",
CollectionType: "playlists",
IsFolder: true,
}
}
// playlistError maps core/playlists write errors to HTTP status: ownership or locked -> 403,
// missing/invisible -> 404 (never revealing another user's private playlist), else -> 500.
func (api *Router) playlistError(w http.ResponseWriter, r *http.Request, err error) {
switch {
case errors.Is(err, model.ErrNotAuthorized), errors.Is(err, model.ErrPlaylistNotEditable):
http.Error(w, "Forbidden", http.StatusForbidden)
case errors.Is(err, model.ErrNotFound):
http.Error(w, "Not Found", http.StatusNotFound)
default:
api.internalError(w, r, err)
}
}
type createPlaylistRequest struct {
Name string `json:"Name"`
Ids []string `json:"Ids"`
MediaType string `json:"MediaType"`
}
// createPlaylist always creates a new playlist (playlistId "" tells core/playlists.Create not to
// replace an existing one), owned by the authenticated user.
func (api *Router) createPlaylist(w http.ResponseWriter, r *http.Request) {
var body createPlaylistRequest
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {View on GitHub (pinned to 4ed7494a32)
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
Example fix
// before: editing another user's playlist as service account -> 403
adminClient.Delete("/Items/" + userPlaylistID)
// after: act as the owner or clone it
userClient := NewClient(tokenFor(playlistOwner))
userClient.Delete("/Items/" + playlistID) Defensive patterns
Strategy: fallback
Try / catch
resp, err := client.Delete("/Items/" + playlistID)
if err == nil && resp.StatusCode == http.StatusForbidden {
// not owner or locked playlist: clone under own account, then edit the copy
newID := client.ClonePlaylist(playlistID)
return client.Delete("/Items/" + newID)
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: "You do not have permission" / 403 Forbidden errors: authenticated but not allowed — causes and fixes across open-source libraries — this error's family across 31 libraries.
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
AI-assisted analysis of navidrome/navidrome@4ed7494a32 (2026-09-01).
Data as JSON: /api/errors/2a9760a7ae7e30c6.
Report an issue: GitHub.