{"record":{"id":"002bdade96d65639","repo":"Kareadita/Kavita","slug":"generic-error","errorCode":null,"errorMessage":"generic-error","messagePattern":"generic-error","errorType":"exception","errorClass":"KavitaException","httpStatus":null,"severity":"error","filePath":"Kavita.Services/AnnotationService.cs","lineNumber":142,"sourceCode":"            annotation.CommentPlainText = StripHtml(dto.CommentHtml);\n\n            unitOfWork.AnnotationRepository.Update(annotation);\n\n            if (!unitOfWork.HasChanges() || await unitOfWork.CommitAsync(ct))\n            {\n                dto = (await unitOfWork.AnnotationRepository.GetAnnotationDto(annotation.Id, ct))!;\n\n                await eventHub.SendMessageToAsync(MessageFactory.AnnotationUpdate,\n                    MessageFactory.AnnotationUpdateEvent(dto), userId, ct);\n\n                return dto;\n            }\n        } catch (Exception ex)\n        {\n            logger.LogError(ex, \"There was an exception updating Annotation for Chapter {ChapterId} - Page {PageNumber}\",  dto.ChapterId, dto.PageNumber);\n        }\n\n        throw new KavitaException(\"generic-error\");\n    }\n\n    public async Task<string> ExportAnnotations(int userId, IList<int>? annotationIds = null,\n        CancellationToken ct = default)\n    {\n        try\n        {\n            // Get all annotations for the user with related data\n            IList<FullAnnotationDto> annotations;\n            if (annotationIds == null)\n            {\n                annotations = await unitOfWork.AnnotationRepository.GetFullAnnotationsByUserIdAsync(userId, ct);\n            }\n            else\n            {\n                annotations = await unitOfWork.AnnotationRepository.GetFullAnnotations(userId, annotationIds, ct);\n            }\n","sourceCodeStart":124,"sourceCodeEnd":160,"githubUrl":"https://github.com/Kareadita/Kavita/blob/9c3e5400007f8a0282f7d883f2ad5e71716e514d/Kavita.Services/AnnotationService.cs#L124-L160","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before — no-change path and real failures both become generic-error\nif (!unitOfWork.HasChanges() || await unitOfWork.CommitAsync(ct)) { /* return dto */ }\n// ...\nthrow new KavitaException(\"generic-error\");\n\n// after — treat no-op as success, propagate auth errors\nif (!unitOfWork.HasChanges()) { return await unitOfWork.AnnotationRepository.GetAnnotationDto(annotation.Id, ct); }\nif (await unitOfWork.CommitAsync(ct)) { /* return dto */ }\nthrow new KavitaException(\"generic-error\");","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"catch (KavitaException ex) { return BadRequest(await localizationService.TranslateAsync(UserId, ex.Message)); }\n// Distinguish 'no changes' from real failure on the service side:\nif (!unitOfWork.HasChanges()) { return existing; } // not an error","preventionTips":["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."],"tags":["annotation","catch-all","database","error-masking"],"backgroundTag":null,"analyzedSha":"9c3e5400007f8a0282f7d883f2ad5e71716e514d","analyzedAt":"2026-08-13T19:06:05.897Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}