Kareadita/Kavita · error · KavitaException

cant-assign-devices-to-default

Error message

cant-assign-devices-to-default

What it means

Thrown by ReadingProfileService.SetProfileDevices when the target profile's Kind is ReadingProfileKind.Default. The Default profile is the system-managed fallback profile that every user inherits; it is intentionally not allowed to carry device bindings because it must match all unscoped reading. Assigning devices to it would create an ambiguous device-scoped rule on a profile that is meant to be device-agnostic.

Source

Thrown at Kavita.Services/Reading/ReadingProfileService.cs:306

            .ProjectTo<UserReadingProfileDto>(mapper.ConfigurationProvider)
            .ToListAsync();
    }

    public Task<List<UserReadingProfileDto>> GetReadingProfileDtosForSeries(int userId, int seriesId)
    {
        return unitOfWork.DataContext.AppUserReadingProfiles
            .Where(rp => rp.AppUserId == userId && rp.SeriesIds.Contains(seriesId))
            .Where(rp => rp.Kind == ReadingProfileKind.User)
            .ProjectTo<UserReadingProfileDto>(mapper.ConfigurationProvider)
            .ToListAsync();
    }

    public async Task SetProfileDevices(int userId, int profileId, List<int> deviceIds)
    {
        var profile = await unitOfWork.AppUserReadingProfileRepository.GetUserProfile(userId, profileId);
        if (profile == null) throw new KavitaException("profile-doesnt-exist");

        if (profile.Kind == ReadingProfileKind.Default) throw new KavitaException("cant-assign-devices-to-default");

        profile.DeviceIds = deviceIds;
        unitOfWork.AppUserReadingProfileRepository.Update(profile);

        await unitOfWork.CommitAsync();

        // Remove series & library links from profiles where there is now overlap with devices
        // E.g. for the same series there are now two profiles that would match
        var profiles = await unitOfWork.AppUserReadingProfileRepository.GetProfilesForUser(userId);

        var overlappingProfiles = profiles
            .Where(rp => rp.Id != profileId)
            .Where(rp => rp.Kind == ReadingProfileKind.User)
            .Where(rp => (rp.DeviceIds.Count == 0 && deviceIds.Count == 0)
                         || rp.DeviceIds.Intersect(deviceIds).Any())
            .Where(rp => rp.SeriesIds.Intersect(profile.SeriesIds).Any()
                         || rp.LibraryIds.Intersect(profile.LibraryIds).Any());

View on GitHub (pinned to 9c3e540000)

Solutions

  1. Filter the profile selector in the UI to only profiles where Kind == User before invoking SetProfileDevices.
  2. Guard the API call by fetching the profile's Kind first and short-circuiting when it is Default.
  3. If devices truly must be scoped, create a new User reading profile and assign devices there instead of reusing the Default profile.
  4. Reproduce with the user's actual profile list to confirm which profileId was passed and correct the caller.

Example fix

// before
await readingProfileService.SetProfileDevices(userId, selectedProfileId, deviceIds);

// after
var profile = await unitOfWork.AppUserReadingProfileRepository.GetUserProfile(userId, selectedProfileId);
if (profile?.Kind == ReadingProfileKind.Default)
{
    // surface 'cant-assign-devices-to-default' to the user as a UI hint, do not call the service
    return BadRequest("Devices cannot be attached to the Default profile");
}
await readingProfileService.SetProfileDevices(userId, selectedProfileId, deviceIds);
Defensive patterns

Strategy: validation

Validate before calling

var profile = await unitOfWork.AppUserReadingProfileRepository.GetUserProfile(userId, profileId);
if (profile is null) return BadRequest("profile-doesnt-exist");
if (profile.Kind == ReadingProfileKind.Default)
    return BadRequest("Devices cannot be attached to the Default profile");
// safe to call SetProfileDevices

Type guard

static bool CanAssignDevices(AppUserReadingProfile p) => p is not null && p.Kind != ReadingProfileKind.Default;

Prevention

When it happens

Trigger: Calling SetProfileDevices(userId, profileId, deviceIds) where profileId resolves to the user's Default profile (Kind == ReadingProfileKind.Default). This happens when the UI/API passes the Default profile's id instead of a User profile id, e.g. POSTing to the set-devices endpoint with the id of the auto-created default profile.

Common situations: Frontend lists all profiles in one dropdown and the user picks the Default one; a migration created a Default profile whose id leaked into a device-assignment payload; or a client hard-codes profileId=0 / first-profile-id which maps to Default.

Related errors


AI-assisted analysis of Kareadita/Kavita@9c3e540000 (2026-08-13). Data as JSON: /api/errors/4f4bc1792c177a92. Report an issue: GitHub.