{"record":{"id":"83d8d37ff200eba5","repo":"Kareadita/Kavita","slug":"annotation-export-failed","errorCode":null,"errorMessage":"annotation-export-failed","messagePattern":"annotation-export-failed","errorType":"exception","errorClass":"KavitaException","httpStatus":null,"severity":"error","filePath":"Kavita.Services/AnnotationService.cs","lineNumber":241,"sourceCode":"                                    obsidianTags = new[] { \"#kavita\", $\"#{seriesGroup.Key.SeriesName.ToLowerInvariant().Replace(\" \", \"-\")}\", \"#highlights\" },\n                                    obsidianTitle,\n                                    obsidianBacklinks = new[] { $\"[[{seriesGroup.Key.SeriesName} Series]]\", $\"[[{volumeGroup.Key.VolumeName}]]\" }\n                                };\n                            }).ToArray(),\n                        }).ToArray(),\n                }).ToArray();\n\n            // Serialize to JSON\n            var json = JsonSerializer.Serialize(exportData, ExportJsonSerializerOptions);\n\n            logger.LogInformation(\"Successfully exported {AnnotationCount} annotations for user {UserId}\", annotations.Count, userId);\n\n            return json;\n        }\n        catch (Exception ex)\n        {\n            logger.LogError(ex, \"Failed to export annotations for user {UserId}\", userId);\n            throw new KavitaException(\"annotation-export-failed\");\n        }\n    }\n\n    private string StripHtml(string? html)\n    {\n        if (string.IsNullOrEmpty(html))\n        {\n            return string.Empty;\n        }\n\n        try\n        {\n            var document = new HtmlDocument();\n            document.LoadHtml(html);\n\n            return document.DocumentNode.InnerText.Replace(\"&nbsp;\", \" \");\n        }\n        catch (Exception exception)","sourceCodeStart":223,"sourceCodeEnd":259,"githubUrl":"https://github.com/Kareadita/Kavita/blob/9c3e5400007f8a0282f7d883f2ad5e71716e514d/Kavita.Services/AnnotationService.cs#L223-L259","documentation":"Catch-all thrown by AnnotationService.ExportAnnotations (line 241) for any exception during the export pipeline — querying annotations, resolving user highlight-slot preferences, building the deep-link hostname, grouping/serialization, or a KeyNotFoundException on users[annotation.UserId]. The original exception is logged with the UserId; the client receives the opaque 'annotation-export-failed' code. Triggered via POST /api/annotation/export or /api/annotation/export-filter.","triggerScenarios":"POST /api/annotation/export (or export-filter) that fails: a user referenced by an annotation has no UserPreferences (users[annotation.UserId] throws KeyNotFoundException), settings.HostName is malformed, JSON serialization of the anonymous projection fails, or a DB read error occurs.","commonSituations":"An annotation whose AppUser was deleted but whose row remains (stale foreign key) → users dictionary lookup throws; very large export exhausting memory during JsonSerializer.Serialize; missing ServerSettings row.","solutions":["Inspect the logged 'ex' for the exact failure (KeyNotFoundException is common).","Use users.GetValueOrDefault(annotation.UserId) instead of the indexer to tolerate a missing user.","If the export is very large, stream the JSON or paginate instead of materializing one big anonymous array.","Ensure all annotation owners still exist as users with UserPreferences."],"exampleFix":"// before — throws KeyNotFoundException if a user is missing\nvar user = users[annotation.UserId];\n\n// after — tolerate a missing user\nif (!users.TryGetValue(annotation.UserId, out var user)) continue;","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"catch (KavitaException ex) { return BadRequest(await localizationService.TranslateAsync(UserId, ex.Message)); }\n// In the service, use a tolerant lookup to avoid KeyNotFoundException:\nif (!users.TryGetValue(annotation.UserId, out var user)) continue;","preventionTips":["Keep annotation owners and their UserPreferences rows consistent (cascade delete or cleanup orphan annotations).","For large libraries, stream/paginate the export instead of building one big in-memory JSON.","Correlate the export 400 with the logged exception (UserId) to find the failing step."],"tags":["annotation","export","catch-all","serialization","error-masking"],"backgroundTag":null,"analyzedSha":"9c3e5400007f8a0282f7d883f2ad5e71716e514d","analyzedAt":"2026-08-13T19:06:05.897Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}