{"record":{"id":"4f4bc1792c177a92","repo":"Kareadita/Kavita","slug":"cant-assign-devices-to-default","errorCode":null,"errorMessage":"cant-assign-devices-to-default","messagePattern":"cant-assign-devices-to-default","errorType":"exception","errorClass":"KavitaException","httpStatus":500,"severity":"error","filePath":"Kavita.Services/Reading/ReadingProfileService.cs","lineNumber":306,"sourceCode":"            .ProjectTo<UserReadingProfileDto>(mapper.ConfigurationProvider)\n            .ToListAsync();\n    }\n\n    public Task<List<UserReadingProfileDto>> GetReadingProfileDtosForSeries(int userId, int seriesId)\n    {\n        return unitOfWork.DataContext.AppUserReadingProfiles\n            .Where(rp => rp.AppUserId == userId && rp.SeriesIds.Contains(seriesId))\n            .Where(rp => rp.Kind == ReadingProfileKind.User)\n            .ProjectTo<UserReadingProfileDto>(mapper.ConfigurationProvider)\n            .ToListAsync();\n    }\n\n    public async Task SetProfileDevices(int userId, int profileId, List<int> deviceIds)\n    {\n        var profile = await unitOfWork.AppUserReadingProfileRepository.GetUserProfile(userId, profileId);\n        if (profile == null) throw new KavitaException(\"profile-doesnt-exist\");\n\n        if (profile.Kind == ReadingProfileKind.Default) throw new KavitaException(\"cant-assign-devices-to-default\");\n\n        profile.DeviceIds = deviceIds;\n        unitOfWork.AppUserReadingProfileRepository.Update(profile);\n\n        await unitOfWork.CommitAsync();\n\n        // Remove series & library links from profiles where there is now overlap with devices\n        // E.g. for the same series there are now two profiles that would match\n        var profiles = await unitOfWork.AppUserReadingProfileRepository.GetProfilesForUser(userId);\n\n        var overlappingProfiles = profiles\n            .Where(rp => rp.Id != profileId)\n            .Where(rp => rp.Kind == ReadingProfileKind.User)\n            .Where(rp => (rp.DeviceIds.Count == 0 && deviceIds.Count == 0)\n                         || rp.DeviceIds.Intersect(deviceIds).Any())\n            .Where(rp => rp.SeriesIds.Intersect(profile.SeriesIds).Any()\n                         || rp.LibraryIds.Intersect(profile.LibraryIds).Any());\n","sourceCodeStart":288,"sourceCodeEnd":324,"githubUrl":"https://github.com/Kareadita/Kavita/blob/9c3e5400007f8a0282f7d883f2ad5e71716e514d/Kavita.Services/Reading/ReadingProfileService.cs#L288-L324","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Filter the profile selector in the UI to only profiles where Kind == User before invoking SetProfileDevices.","Guard the API call by fetching the profile's Kind first and short-circuiting when it is Default.","If devices truly must be scoped, create a new User reading profile and assign devices there instead of reusing the Default profile.","Reproduce with the user's actual profile list to confirm which profileId was passed and correct the caller."],"exampleFix":"// before\nawait readingProfileService.SetProfileDevices(userId, selectedProfileId, deviceIds);\n\n// after\nvar profile = await unitOfWork.AppUserReadingProfileRepository.GetUserProfile(userId, selectedProfileId);\nif (profile?.Kind == ReadingProfileKind.Default)\n{\n    // surface 'cant-assign-devices-to-default' to the user as a UI hint, do not call the service\n    return BadRequest(\"Devices cannot be attached to the Default profile\");\n}\nawait readingProfileService.SetProfileDevices(userId, selectedProfileId, deviceIds);","handlingStrategy":"validation","validationCode":"var profile = await unitOfWork.AppUserReadingProfileRepository.GetUserProfile(userId, profileId);\nif (profile is null) return BadRequest(\"profile-doesnt-exist\");\nif (profile.Kind == ReadingProfileKind.Default)\n    return BadRequest(\"Devices cannot be attached to the Default profile\");\n// safe to call SetProfileDevices","typeGuard":"static bool CanAssignDevices(AppUserReadingProfile p) => p is not null && p.Kind != ReadingProfileKind.Default;","tryCatchPattern":null,"preventionTips":["Filter the profile picker UI to Kind == User profiles only.","Never reuse the Default profile's id in device-assignment payloads.","Log profile.Kind before calling SetProfileDevices during debugging."],"tags":["reading-profiles","validation","business-rule","device-binding"],"backgroundTag":null,"analyzedSha":"9c3e5400007f8a0282f7d883f2ad5e71716e514d","analyzedAt":"2026-08-13T19:06:05.897Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}