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

  1. Check the server log — the catch at line 137 logs 'ex' with ChapterId/PageNumber; the generic client message hides the real cause.
  2. If 'no changes' is expected behavior, treat HasChanges()==false as success (return the existing dto) rather than a failure.
  3. Refactor to rethrow KavitaException so 'denied' (error 23) is distinguishable from a genuine DB failure.
  4. 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

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.