Kareadita/Kavita · warning · KavitaException
external-source-already-exists
Error message
external-source-already-exists
What it means
Thrown by StreamService.CreateExternalSource when the user already has an AppUserExternalSource whose Host equals the raw dto.Host. Host uniqueness is enforced per user. Gotcha: the duplicate check compares the RAW dto.Host against the stored Host, but stored hosts are normalized on save via EnsureStartsWithHttpOrHttps + EnsureEndsWithSlash, so the same logical host entered with a different trailing-slash or scheme can slip past this check. It is a localized KavitaException surfaced as HTTP 500.
Source
Thrown at Kavita.Services/StreamService.cs:294
user.SideNavStreams = list;
unitOfWork.UserRepository.Update(user);
await unitOfWork.CommitAsync(ct);
if (!stream.Visible) return;
await eventHub.SendMessageToAsync(MessageFactory.SideNavUpdate, MessageFactory.SideNavUpdateEvent(userId),
userId, ct);
}
public async Task<ExternalSourceDto> CreateExternalSource(int userId, ExternalSourceDto dto,
CancellationToken ct = default)
{
var user = await unitOfWork.UserRepository.GetUserByIdAsync(userId,
AppUserIncludes.ExternalSources, ct);
if (user == null) throw new KavitaException("not-authenticated");
if (user.ExternalSources.Any(s => s.Host == dto.Host))
{
throw new KavitaException("external-source-already-exists");
}
if (string.IsNullOrEmpty(dto.Name)) throw new KavitaException("external-source-required");
if (!UrlHelper.StartsWithHttpOrHttps(dto.Host)) throw new KavitaException("external-source-host-format");
var newSource = new AppUserExternalSource()
{
Name = dto.Name,
Host = UrlHelper.EnsureEndsWithSlash(UrlHelper.EnsureStartsWithHttpOrHttps(dto.Host)),
ApiKey = dto.ApiKey
};
user.ExternalSources.Add(newSource);
unitOfWork.UserRepository.Update(user);
await unitOfWork.CommitAsync(ct);
dto.Id = newSource.Id;View on GitHub (pinned to 9c3e540000)
Solutions
- Pre-check uniqueness with GET /api/streams/external-sources and compare normalized hosts (lower-case, scheme + trailing slash) before enabling Save.
- Normalize the host input the same way the server does (force https, append trailing slash) before the duplicate comparison so equivalent hosts are detected.
- Surface a clear inline error when a duplicate is detected rather than submitting.
Example fix
// before
save(dto: ExternalSourceDto) {
return this.http.post('streams/create-external-source', dto);
}
// after
save(dto: ExternalSourceDto) {
const norm = normalizeHost(dto.host); // https + trailing slash
if (this.sources.some(s => normalizeHost(s.host) === norm)) {
return throwError(() => new Error('External source already exists'));
}
return this.http.post('streams/create-external-source', { ...dto, host: norm });
} Defensive patterns
Strategy: validation
Validate before calling
function normalizeHost(host: string): string {
let h = host.trim().toLowerCase();
if (!/^https?:\/\//.test(h)) h = 'https://' + h;
if (!h.endsWith('/')) h += '/';
return h;
}
function isDuplicateExternalSource(existing: ExternalSourceDto[], host: string): boolean {
const norm = normalizeHost(host);
return existing.some(s => normalizeHost(s.host) === norm);
} Prevention
- Normalize host exactly as the server stores it (force https, trailing slash) before duplicate checks.
- Load the external-sources list before showing the create form.
- Show an inline duplicate warning instead of submitting.
When it happens
Trigger: POST /api/streams/create-external-source with a dto whose Host matches an existing AppUserExternalSource.Host for the calling user exactly (byte-for-byte, as previously normalized).
Common situations: User re-adds an external server they already configured; copy-pasting the same host twice; the UI failed to reflect a prior successful save.
Related errors
- external-source-already-in-use
- collection-tag-duplicate
- name-already-in-use
- errors.theme-already-in-use
- smart-filter-already-in-use
AI-assisted analysis of Kareadita/Kavita@9c3e540000 (2026-08-13).
Data as JSON: /api/errors/6ad7c5fe1d975418.
Report an issue: GitHub.