{"record":{"id":"f4d669d7246ace7c","repo":"Kareadita/Kavita","slug":"denied","errorCode":null,"errorMessage":"denied","messagePattern":"denied","errorType":"exception","errorClass":"KavitaException","httpStatus":null,"severity":"error","filePath":"Kavita.Services/AnnotationService.cs","lineNumber":118,"sourceCode":"            logger.LogError(ex, \"There was an exception when creating an annotation on {ChapterId} - Page {Page}\", dto.ChapterId, dto.PageNumber);\n            throw new KavitaException(\"annotation-failed-create\");\n        }\n    }\n\n    /// <summary>\n    /// Update the modifiable fields (Spoiler, highlight slot, and comment) for an annotation\n    /// </summary>\n    /// <param name=\"userId\"></param>\n    /// <param name=\"dto\"></param>\n    /// <param name=\"ct\"></param>\n    /// <returns></returns>\n    /// <exception cref=\"KavitaException\">Message is not localized</exception>\n    public async Task<AnnotationDto> UpdateAnnotation(int userId, AnnotationDto dto, CancellationToken ct = default)\n    {\n        try\n        {\n            var annotation = await unitOfWork.AnnotationRepository.GetAnnotation(dto.Id, ct);\n            if (annotation == null || annotation.AppUserId != userId) throw new KavitaException(\"denied\");\n\n            annotation.ContainsSpoiler = dto.ContainsSpoiler;\n            annotation.SelectedSlotIndex = dto.SelectedSlotIndex;\n            annotation.Comment = dto.Comment;\n            annotation.CommentHtml = dto.CommentHtml;\n            annotation.CommentPlainText = StripHtml(dto.CommentHtml);\n\n            unitOfWork.AnnotationRepository.Update(annotation);\n\n            if (!unitOfWork.HasChanges() || await unitOfWork.CommitAsync(ct))\n            {\n                dto = (await unitOfWork.AnnotationRepository.GetAnnotationDto(annotation.Id, ct))!;\n\n                await eventHub.SendMessageToAsync(MessageFactory.AnnotationUpdate,\n                    MessageFactory.AnnotationUpdateEvent(dto), userId, ct);\n\n                return dto;\n            }","sourceCodeStart":100,"sourceCodeEnd":136,"githubUrl":"https://github.com/Kareadita/Kavita/blob/9c3e5400007f8a0282f7d883f2ad5e71716e514d/Kavita.Services/AnnotationService.cs#L100-L136","documentation":"Thrown by AnnotationService.UpdateAnnotation when the annotation for dto.Id is not found OR its AppUserId does not match the requesting userId — an ownership/authorization guard that intentionally collapses 'missing' and 'not-yours' into one 'denied'. NOTE: the throw is inside the method's try (line 115) and is caught by catch (Exception) at line 137, which only logs and then falls through to throw 'generic-error' at line 142. So the caller never receives 'denied'; it always surfaces as 'generic-error'.","triggerScenarios":"POST /api/annotation/update with an annotation Id that does not exist, belongs to another user, or was deleted. Also when a user tries to edit an annotation they only 'liked' rather than own.","commonSituations":"Concurrent deletion by another session/device; stale UI listing an annotation the user no longer owns; permission model where only the owner may edit and the client mistakenly enables edit for non-owners.","solutions":["Confirm the annotation Id being edited is still owned by the current user before issuing the update.","Stop masking: refactor UpdateAnnotation so KavitaException is rethrown (catch KavitaException { throw; }) so 'denied' reaches the client as a clear 400/403.","Have the controller translate 'denied' into 403 Forbidden instead of 400 BadRequest for clearer semantics."],"exampleFix":"// before — 'denied' is caught by catch(Exception) and becomes 'generic-error'\nif (annotation == null || annotation.AppUserId != userId) throw new KavitaException(\"denied\");\n// ...\n} catch (Exception ex) { logger.LogError(ex, ...); }\nthrow new KavitaException(\"generic-error\");\n\n// after — let the authorization result propagate\ncatch (KavitaException) { throw; }\ncatch (Exception ex) { logger.LogError(ex, ...); }\nthrow new KavitaException(\"generic-error\");","handlingStrategy":"validation","validationCode":"// Before update, confirm ownership on the client using known owner info\nif (annotation.OwnerUserId != currentUserId) { /* do not call update */ }\n// Server-side pre-check is what the service itself does; just ensure the id is current.","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Only enable 'edit' UI for annotations the user owns.","Refetch the annotation list before editing to avoid stale ids.","Be aware 'denied' currently surfaces as 'generic-error' due to the catch-all; check logs for the ownership failure."],"tags":["annotation","authorization","ownership","error-masking"],"backgroundTag":null,"analyzedSha":"9c3e5400007f8a0282f7d883f2ad5e71716e514d","analyzedAt":"2026-08-13T19:06:05.897Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}