Kareadita/Kavita · error · KavitaException
generic-error
Error message
generic-error
What it means
The terminal throw at the end of AnnotationService.UpdateAnnotation (line 142). It is reached in two cases: (1) the commit was a no-op — unitOfWork.HasChanges() is false AND CommitAsync returned false, or (2) any exception was caught by the catch-all at line 137 (including the masked 'denied'). It is the method's catch-all/fallthrough for update failure; the client always sees 'generic-error' for a failed update.
Source
Thrown at Kavita.Services/AnnotationService.cs:142
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;
}
} catch (Exception ex)
{
logger.LogError(ex, "There was an exception updating Annotation for Chapter {ChapterId} - Page {PageNumber}", dto.ChapterId, dto.PageNumber);
}
throw new KavitaException("generic-error");
}
public async Task<string> ExportAnnotations(int userId, IList<int>? annotationIds = null,
CancellationToken ct = default)
{
try
{
// Get all annotations for the user with related data
IList<FullAnnotationDto> annotations;
if (annotationIds == null)
{
annotations = await unitOfWork.AnnotationRepository.GetFullAnnotationsByUserIdAsync(userId, ct);
}
else
{
annotations = await unitOfWork.AnnotationRepository.GetFullAnnotations(userId, annotationIds, ct);
}
View on GitHub (pinned to 9c3e540000)
Solutions
- Check the server log — the catch at line 137 logs 'ex' with ChapterId/PageNumber; the generic client message hides the real cause.
- If 'no changes' is expected behavior, treat HasChanges()==false as success (return the existing dto) rather than a failure.
- Refactor to rethrow KavitaException so 'denied' (error 23) is distinguishable from a genuine DB failure.
- Retry on transient SQLite 'database is locked' by ensuring WAL mode and avoiding long transactions.
Example fix
// before — no-change path and real failures both become generic-error
if (!unitOfWork.HasChanges() || await unitOfWork.CommitAsync(ct)) { /* return dto */ }
// ...
throw new KavitaException("generic-error");
// after — treat no-op as success, propagate auth errors
if (!unitOfWork.HasChanges()) { return await unitOfWork.AnnotationRepository.GetAnnotationDto(annotation.Id, ct); }
if (await unitOfWork.CommitAsync(ct)) { /* return dto */ }
throw new KavitaException("generic-error"); Defensive patterns
Strategy: try-catch
Try / catch
catch (KavitaException ex) { return BadRequest(await localizationService.TranslateAsync(UserId, ex.Message)); }
// Distinguish 'no changes' from real failure on the service side:
if (!unitOfWork.HasChanges()) { return existing; } // not an error Prevention
- Suppress the update call when the dto values equal the current annotation (avoids the no-op failure path).
- Treat generic-error as a signal to inspect server logs (ChapterId/PageNumber logged at line 139).
- Re-throw KavitaException in the service so 'denied' is distinguishable from a genuine DB failure.
When it happens
Trigger: POST /api/annotation/update where the DB commit fails, the entity has no effective changes, or the annotation lookup/ownership throws. Controller maps this to HTTP 400 with a localized 'generic-error' message.
Common situations: User submits an update identical to current values (no changes → HasChanges false, CommitAsync false); SQLite busy/locked during CommitAsync; the annotation row was concurrently removed.
Related errors
AI-assisted analysis of Kareadita/Kavita@9c3e540000 (2026-08-13).
Data as JSON: /api/errors/002bdade96d65639.
Report an issue: GitHub.