Kareadita/Kavita · error · KavitaException

annotation-failed-create

Error message

annotation-failed-create

What it means

The catch-all thrown at the end of AnnotationService.CreateAnnotation's catch (Exception ex) block (line 101). It fires for ANY unhandled exception during annotation creation — DB constraint violation, Attach/CommitAsync failure, the deliberately-thrown 'invalid-payload'/'chapter-doesnt-exist', or a bug in the bookService TOC lookup. The original exception (ex) is logged with ChapterId and PageNumber, but only the opaque 'annotation-failed-create' code is returned to the client. Because of this, the two more specific codes (errors 20, 21) are effectively unreachable to the caller.

Source

Thrown at Kavita.Services/AnnotationService.cs:101

                CommentHtml = dto.CommentHtml,
                CommentPlainText = StripHtml(dto.CommentHtml),
                ContainsSpoiler = dto.ContainsSpoiler,
                PageNumber = dto.PageNumber,
                SelectedSlotIndex = dto.SelectedSlotIndex,
                AppUserId = userId,
                Context = dto.Context,
                ChapterTitle = chapterTitle
            };

            unitOfWork.AnnotationRepository.Attach(annotation);
            await unitOfWork.CommitAsync(ct);

            return (await unitOfWork.AnnotationRepository.GetAnnotationDto(annotation.Id, ct))!;
        }
        catch (Exception ex)
        {
            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");

View on GitHub (pinned to 9c3e540000)

Solutions

  1. Read the server log for the logged 'ex' — the message key 'annotation-failed-create' is intentionally generic; the real cause is in the exception stack logged on the same line.
  2. If it reproduces, reproduce with one user and check for DB unique/foreign-key errors on AppUserAnnotation.
  3. Refactor to let known business KavitaExceptions propagate (rethrow) so clients get actionable codes instead of the generic one.
  4. Ensure the SQLite DB is not on a network share / locked volume where CommitAsync can fail.

Example fix

// before — single catch masks every failure
}catch (Exception ex){
    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");
}

// after — surface business errors, wrap only unexpected ones
catch (KavitaException) { throw; }
catch (DbUpdateException ex){ logger.LogError(ex, ...); throw new KavitaException("annotation-failed-create"); }
catch (Exception ex){ logger.LogError(ex, ...); throw new KavitaException("annotation-failed-create"); }
Defensive patterns

Strategy: try-catch

Try / catch

// Controller-level: this is already the pattern in AnnotationController
catch (KavitaException ex) {
    // 'annotation-failed-create' -> localize + 400; real cause is in server logs
    return BadRequest(await localizationService.TranslateAsync(UserId, ex.Message));
}
// For the service: distinguish business vs unexpected errors
catch (KavitaException) { throw; }
catch (DbUpdateException ex) { logger.LogError(ex, ...); throw new KavitaException("annotation-failed-create"); }
catch (Exception ex) { logger.LogError(ex, ...); throw new KavitaException("annotation-failed-create"); }

Prevention

When it happens

Trigger: POST /api/annotation/create that hits any runtime failure: unique-constraint on the annotation, EF Core CommitAsync DB error, null deref during TOC/title resolution, or the inner validation throws. The controller (AnnotationController.CreateAnnotation) maps this KavitaException to HTTP 400 with a localized message.

Common situations: Duplicate annotation submission (same XPath/page) racing a DB unique index; SQLite locked/busy under concurrent writes; transient DB disconnect during CommitAsync; an unexpected null from GetAnnotationDto after insert.

Related errors


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