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
- 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.
- If it reproduces, reproduce with one user and check for DB unique/foreign-key errors on AppUserAnnotation.
- Refactor to let known business KavitaExceptions propagate (rethrow) so clients get actionable codes instead of the generic one.
- 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
- Always correlate the client 400 with the server log entry (same ChapterId/PageNumber) to find the real exception.
- Avoid concurrent duplicate annotation inserts that hit the DB unique constraint.
- Keep SQLite in WAL mode to reduce 'database is locked' during CommitAsync.
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.