{"record":{"id":"acdb053adc92c0fb","repo":"Kareadita/Kavita","slug":"annotation-failed-create","errorCode":null,"errorMessage":"annotation-failed-create","messagePattern":"annotation-failed-create","errorType":"exception","errorClass":"KavitaException","httpStatus":null,"severity":"error","filePath":"Kavita.Services/AnnotationService.cs","lineNumber":101,"sourceCode":"                CommentHtml = dto.CommentHtml,\n                CommentPlainText = StripHtml(dto.CommentHtml),\n                ContainsSpoiler = dto.ContainsSpoiler,\n                PageNumber = dto.PageNumber,\n                SelectedSlotIndex = dto.SelectedSlotIndex,\n                AppUserId = userId,\n                Context = dto.Context,\n                ChapterTitle = chapterTitle\n            };\n\n            unitOfWork.AnnotationRepository.Attach(annotation);\n            await unitOfWork.CommitAsync(ct);\n\n            return (await unitOfWork.AnnotationRepository.GetAnnotationDto(annotation.Id, ct))!;\n        }\n        catch (Exception ex)\n        {\n            logger.LogError(ex, \"There was an exception when creating an annotation on {ChapterId} - Page {Page}\", dto.ChapterId, dto.PageNumber);\n            throw new KavitaException(\"annotation-failed-create\");\n        }\n    }\n\n    /// <summary>\n    /// Update the modifiable fields (Spoiler, highlight slot, and comment) for an annotation\n    /// </summary>\n    /// <param name=\"userId\"></param>\n    /// <param name=\"dto\"></param>\n    /// <param name=\"ct\"></param>\n    /// <returns></returns>\n    /// <exception cref=\"KavitaException\">Message is not localized</exception>\n    public async Task<AnnotationDto> UpdateAnnotation(int userId, AnnotationDto dto, CancellationToken ct = default)\n    {\n        try\n        {\n            var annotation = await unitOfWork.AnnotationRepository.GetAnnotation(dto.Id, ct);\n            if (annotation == null || annotation.AppUserId != userId) throw new KavitaException(\"denied\");\n","sourceCodeStart":83,"sourceCodeEnd":119,"githubUrl":"https://github.com/Kareadita/Kavita/blob/9c3e5400007f8a0282f7d883f2ad5e71716e514d/Kavita.Services/AnnotationService.cs#L83-L119","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before — single catch masks every failure\n}catch (Exception ex){\n    logger.LogError(ex, \"There was an exception when creating an annotation on {ChapterId} - Page {Page}\", dto.ChapterId, dto.PageNumber);\n    throw new KavitaException(\"annotation-failed-create\");\n}\n\n// after — surface business errors, wrap only unexpected ones\ncatch (KavitaException) { throw; }\ncatch (DbUpdateException ex){ logger.LogError(ex, ...); throw new KavitaException(\"annotation-failed-create\"); }\ncatch (Exception ex){ logger.LogError(ex, ...); throw new KavitaException(\"annotation-failed-create\"); }","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"// Controller-level: this is already the pattern in AnnotationController\ncatch (KavitaException ex) {\n    // 'annotation-failed-create' -> localize + 400; real cause is in server logs\n    return BadRequest(await localizationService.TranslateAsync(UserId, ex.Message));\n}\n// For the service: distinguish business vs unexpected errors\ncatch (KavitaException) { throw; }\ncatch (DbUpdateException ex) { logger.LogError(ex, ...); throw new KavitaException(\"annotation-failed-create\"); }\ncatch (Exception ex) { logger.LogError(ex, ...); throw new KavitaException(\"annotation-failed-create\"); }","preventionTips":["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."],"tags":["annotation","error-masking","catch-all","database","book-reader"],"backgroundTag":null,"analyzedSha":"9c3e5400007f8a0282f7d883f2ad5e71716e514d","analyzedAt":"2026-08-13T19:06:05.897Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}