{"record":{"id":"066150eceb2bb084","repo":"Kareadita/Kavita","slug":"invalid-payload","errorCode":"invalid-payload","errorMessage":"invalid-payload","messagePattern":"invalid-payload","errorType":"exception","errorClass":"KavitaException","httpStatus":400,"severity":"warning","filePath":"Kavita.Services/AnnotationService.cs","lineNumber":52,"sourceCode":"        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,\n        Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping\n    };\n\n    /// <summary>\n    /// Create a new Annotation for the user against a Chapter\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> CreateAnnotation(int userId, AnnotationDto dto, CancellationToken ct = default)\n    {\n        try\n        {\n            if (dto.HighlightCount == 0 || string.IsNullOrWhiteSpace(dto.SelectedText))\n            {\n                throw new KavitaException(\"invalid-payload\");\n            }\n\n            var chapter = await unitOfWork.ChapterRepository.GetChapterAsync(dto.ChapterId, ct: ct) ?? throw new KavitaException(\"chapter-doesnt-exist\");\n            var chapterTitle = string.Empty;\n\n            try\n            {\n                var toc = await bookService.GenerateTableOfContents(chapter);\n                var pageTocs = BookChapterItemHelper.GetTocForPage(toc, dto.PageNumber);\n                if (pageTocs.Count > 0)\n                {\n                    chapterTitle = pageTocs[0].Title;\n                }\n            }\n            catch (KavitaException)\n            {\n                /* Swallow */\n            }","sourceCodeStart":34,"sourceCodeEnd":70,"githubUrl":"https://github.com/Kareadita/Kavita/blob/9c3e5400007f8a0282f7d883f2ad5e71716e514d/Kavita.Services/AnnotationService.cs#L34-L70","documentation":"Thrown by AnnotationService.CreateAnnotation when the incoming AnnotationDto carries no real highlight: HighlightCount is 0 OR SelectedText is null/empty/whitespace. It is a pre-flight input guard meant to reject empty-text annotation creation. IMPORTANT: the throw sits inside the method's outer try, whose catch (Exception) at line 98 re-wraps every exception as 'annotation-failed-create', so the API consumer never receives 'invalid-payload' — only the generic create-failure code reaches the controller; the true cause is visible solely in the server log (ex is logged with ChapterId/Page).","triggerScenarios":"POST /api/annotation/create with a body whose highlightCount is 0 or whose selectedText is null/\"\"/whitespace. Also reached if the DTO round-trip loses selectedText (e.g. client sends only XPath).","commonSituations":"Front-end bug that fires the save before the user finishes selecting text; programmatic/scripted client that omits selectedText; a highlight that was cleared (count reset to 0) immediately before the create request.","solutions":["On the client, only call create after SelectedText is non-empty AND HighlightCount > 0.","Add request validation ([Required]/FluentValidation) on AnnotationDto.SelectedText and a [Range(1,...)] on HighlightCount so bad payloads are rejected at the controller boundary with 400.","Refactor CreateAnnotation so the input KavitaException is not swallowed by the catch-all — re-throw it (e.g. catch KavitaException first and rethrow) so the real code surfaces instead of annotation-failed-create."],"exampleFix":"// before (inside the outer try — message is swallowed by catch(Exception) at line 98)\nif (dto.HighlightCount == 0 || string.IsNullOrWhiteSpace(dto.SelectedText))\n    throw new KavitaException(\"invalid-payload\");\n\n// after — validate before the try, or rethrow inside it\nif (dto.HighlightCount == 0 || string.IsNullOrWhiteSpace(dto.SelectedText))\n    throw new KavitaException(\"invalid-payload\");\ntry { /* ... */ }\ncatch (KavitaException) { throw; }            // preserve the specific code\ncatch (Exception ex) { logger.LogError(ex, ...); throw new KavitaException(\"annotation-failed-create\"); }","handlingStrategy":"validation","validationCode":"// Run before annotationService.CreateAnnotation(...)\nif (dto.HighlightCount <= 0 || string.IsNullOrWhiteSpace(dto.SelectedText))\n    return BadRequest(\"Annotation requires selected text and a positive highlight count\");","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Disable the save-annotation UI control until the user has selected non-empty text.","Add [Required] on AnnotationDto.SelectedText and a positive-range validator on HighlightCount at the DTO boundary.","Treat the API's 'annotation-failed-create' as a hint to check server logs, since invalid-payload is masked by the catch-all."],"tags":["validation","annotation","book-reader","input-validation"],"backgroundTag":null,"analyzedSha":"9c3e5400007f8a0282f7d883f2ad5e71716e514d","analyzedAt":"2026-08-13T19:06:05.897Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}