{"record":{"id":"072c5b8e1e0bb1e7","repo":"Kareadita/Kavita","slug":"external-source-already-in-use","errorCode":null,"errorMessage":"external-source-already-in-use","messagePattern":"external-source-already-in-use","errorType":"exception","errorClass":"KavitaException","httpStatus":400,"severity":"warning","filePath":"Kavita.Services/StreamService.cs","lineNumber":201,"sourceCode":"        };\n\n\n        await eventHub.SendMessageToAsync(MessageFactory.SideNavUpdate, MessageFactory.SideNavUpdateEvent(userId),\n            userId, ct);\n        return ret;\n    }\n\n    public async Task<SideNavStreamDto> CreateSideNavStreamFromExternalSource(int userId, int externalSourceId,\n        CancellationToken ct = default)\n    {\n        var user = await unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.SideNavStreams, ct);\n        if (user == null) throw new KavitaException(await localizationService.TranslateAsync(userId, \"no-user\"));\n\n        var externalSource = await unitOfWork.AppUserExternalSourceRepository.GetById(externalSourceId, ct);\n        if (externalSource == null) throw new KavitaException(await localizationService.TranslateAsync(userId, \"external-source-doesnt-exist\"));\n\n        var stream = user?.SideNavStreams.FirstOrDefault(d => d.ExternalSourceId == externalSourceId);\n        if (stream != null) throw new KavitaException(await localizationService.TranslateAsync(userId, \"external-source-already-in-use\"));\n\n        var maxOrder = user!.SideNavStreams.Max(d => d.Order);\n        var createdStream = new AppUserSideNavStream()\n        {\n            Name = externalSource.Name,\n            IsProvided = false,\n            StreamType = SideNavStreamType.ExternalSource,\n            Visible = true,\n            Order = maxOrder + 1,\n            ExternalSourceId = externalSource.Id\n        };\n\n        user.SideNavStreams.Add(createdStream);\n        unitOfWork.UserRepository.Update(user);\n        await unitOfWork.CommitAsync(ct);\n\n        var ret = new SideNavStreamDto()\n        {","sourceCodeStart":183,"sourceCodeEnd":219,"githubUrl":"https://github.com/Kareadita/Kavita/blob/9c3e5400007f8a0282f7d883f2ad5e71716e514d/Kavita.Services/StreamService.cs#L183-L219","documentation":"Thrown by StreamService.CreateSideNavStreamFromExternalSource when the calling user already has an AppUserSideNavStream bound to the same AppUserExternalSource. Kavita allows only one side-nav entry per external source per user, so re-adding the same source is rejected after the user and the source are both validated. It is a localized KavitaException; note the global ExceptionMiddleware routes a plain KavitaException to the generic handler, so the API client actually receives HTTP 500 carrying the user-facing message (not a 400).","triggerScenarios":"POST /api/streams/add-sidenav-stream-from-external-source?externalSourceId=N (StreamController.AddSideNavFromExternalSource) where N already equals the ExternalSourceId of an existing AppUserSideNavStream owned by the user. The throw is on the line `var stream = user?.SideNavStreams.FirstOrDefault(d => d.ExternalSourceId == externalSourceId); if (stream != null) throw ...`.","commonSituations":"Double-clicking the 'Add to sidebar' action for an external source; the side-nav list in the UI is stale and still shows an enabled Add control; a previous add succeeded but the optimistic UI update was rolled back, so the user clicks again.","solutions":["GET /api/streams/sidenav before offering the action, and disable/hide 'Add' when any returned stream already has externalSourceId === N.","Debounce or disable the add button until the HTTP response resolves to prevent duplicate submissions.","If the stream appears missing in the UI, refresh the side-nav (GET /api/streams/sidenav) rather than blindly retrying the add."],"exampleFix":"// before\naddSourceToSideNav(id: number) {\n  return this.http.post(`streams/add-sidenav-stream-from-external-source?externalSourceId=${id}`, {});\n}\n// after\naddSourceToSideNav(id: number) {\n  return this.sidenav$.pipe(\n    take(1),\n    filter(list => !list.some(s => s.externalSourceId === id)),\n    switchMap(() =>\n      this.http.post(`streams/add-sidenav-stream-from-external-source?externalSourceId=${id}`, {}))\n  );\n}","handlingStrategy":"validation","validationCode":"// before adding an external source to the side-nav\nfunction canAddExternalSourceToSideNav(sidenav: SideNavStreamDto[], externalSourceId: number): boolean {\n  return !sidenav.some(s => s.externalSourceId === externalSourceId);\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Cache the current side-nav list and treat any source already present as non-addable.","Disable the Add button immediately on click and re-enable only after the response.","Refresh GET /api/streams/sidenav whenever the add flow is opened to avoid stale state."],"tags":["sidenav","external-source","duplicate","validation"],"backgroundTag":null,"analyzedSha":"9c3e5400007f8a0282f7d883f2ad5e71716e514d","analyzedAt":"2026-08-13T19:06:05.897Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}