Kareadita/Kavita · error · KavitaException

denied

Error message

denied

What it means

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'.

Source

Thrown at Kavita.Services/AnnotationService.cs:118

            logger.LogError(ex, "There was an exception when creating an annotation on {ChapterId} - Page {Page}", dto.ChapterId, dto.PageNumber);
            throw new KavitaException("annotation-failed-create");
        }
    }

    /// <summary>
    /// Update the modifiable fields (Spoiler, highlight slot, and comment) for an annotation
    /// </summary>
    /// <param name="userId"></param>
    /// <param name="dto"></param>
    /// <param name="ct"></param>
    /// <returns></returns>
    /// <exception cref="KavitaException">Message is not localized</exception>
    public async Task<AnnotationDto> UpdateAnnotation(int userId, AnnotationDto dto, CancellationToken ct = default)
    {
        try
        {
            var annotation = await unitOfWork.AnnotationRepository.GetAnnotation(dto.Id, ct);
            if (annotation == null || annotation.AppUserId != userId) throw new KavitaException("denied");

            annotation.ContainsSpoiler = dto.ContainsSpoiler;
            annotation.SelectedSlotIndex = dto.SelectedSlotIndex;
            annotation.Comment = dto.Comment;
            annotation.CommentHtml = dto.CommentHtml;
            annotation.CommentPlainText = StripHtml(dto.CommentHtml);

            unitOfWork.AnnotationRepository.Update(annotation);

            if (!unitOfWork.HasChanges() || await unitOfWork.CommitAsync(ct))
            {
                dto = (await unitOfWork.AnnotationRepository.GetAnnotationDto(annotation.Id, ct))!;

                await eventHub.SendMessageToAsync(MessageFactory.AnnotationUpdate,
                    MessageFactory.AnnotationUpdateEvent(dto), userId, ct);

                return dto;
            }

View on GitHub (pinned to 9c3e540000)

Solutions

  1. Confirm the annotation Id being edited is still owned by the current user before issuing the update.
  2. Stop masking: refactor UpdateAnnotation so KavitaException is rethrown (catch KavitaException { throw; }) so 'denied' reaches the client as a clear 400/403.
  3. Have the controller translate 'denied' into 403 Forbidden instead of 400 BadRequest for clearer semantics.

Example fix

// before — 'denied' is caught by catch(Exception) and becomes 'generic-error'
if (annotation == null || annotation.AppUserId != userId) throw new KavitaException("denied");
// ...
} catch (Exception ex) { logger.LogError(ex, ...); }
throw new KavitaException("generic-error");

// after — let the authorization result propagate
catch (KavitaException) { throw; }
catch (Exception ex) { logger.LogError(ex, ...); }
throw new KavitaException("generic-error");
Defensive patterns

Strategy: validation

Validate before calling

// Before update, confirm ownership on the client using known owner info
if (annotation.OwnerUserId != currentUserId) { /* do not call update */ }
// Server-side pre-check is what the service itself does; just ensure the id is current.

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


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