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

  1. Authenticate as the user who owns the playlist, or create your own copy and edit that
  2. If the playlist is locked/smart, change it via the mechanism that manages it (rules editor or source sync) rather than direct item mutation
  3. Use the admin account if administrative playlist management is required
  4. 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

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.

Related errors


AI-assisted analysis of navidrome/navidrome@4ed7494a32 (2026-09-01). Data as JSON: /api/errors/2a9760a7ae7e30c6. Report an issue: GitHub.