Kareadita/Kavita · warning · KavitaException

errors.theme-already-in-use

errors.theme-already-in-use

Error message

errors.theme-already-in-use

What it means

Thrown by SiteThemeService.CreateThemeFromFile when a theme with the same name already exists. The theme name is derived from the uploaded filename without its extension, and GetThemeDtoByName(themeName) returns non-null. KavitaException('errors.theme-already-in-use') is thrown. ThemeController's upload-theme endpoint does not catch it, so it surfaces as HTTP 500 with the raw key 'errors.theme-already-in-use'.

Source

Thrown at Kavita.Services/SiteThemeService.cs:426

    /// <param name="tempFile"></param>
    /// <param name="username"></param>
    /// <param name="ct"></param>
    /// <returns></returns>
    public async Task<SiteTheme> CreateThemeFromFile(string tempFile, string username, CancellationToken ct = default)
    {
        if (!directoryService.FileSystem.File.Exists(tempFile))
        {
            logger.LogInformation("Unable to create theme from manual upload as file not in temp");
            throw new KavitaException("errors.theme-manual-upload");
        }


        var filename = directoryService.FileSystem.FileInfo.New(tempFile).Name;
        var themeName = Path.GetFileNameWithoutExtension(filename);

        if (await unitOfWork.SiteThemeRepository.GetThemeDtoByName(themeName) != null)
        {
            throw new KavitaException("errors.theme-already-in-use");
        }

        directoryService.CopyFileToDirectory(tempFile, directoryService.SiteThemeDirectory);
        var finalLocation = directoryService.FileSystem.Path.Join(directoryService.SiteThemeDirectory, filename);


        // Create a new entry and note that this is downloaded
        var theme = new SiteTheme()
        {
            Name = Path.GetFileNameWithoutExtension(filename),
            NormalizedName = themeName.ToNormalized(),
            FileName = directoryService.FileSystem.Path.GetFileName(finalLocation),
            Provider = ThemeProvider.Custom,
            IsDefault = false,
            Description = $"Manually uploaded via UI by {username}",
            PreviewUrls = string.Empty,
            Author = username,
        };

View on GitHub (pinned to 9c3e540000)

Solutions

  1. Rename the .css file before uploading so its base name is unique.
  2. List existing themes via GET /api/theme and pick a non-colliding name.
  3. If the existing theme is no longer wanted and not in use, delete it first (see errors.delete-theme-in-use), then re-upload.

Example fix

// before
upload('dark.css') // collides with built-in Dark

// after
upload('dark-custom.css') // unique name
Defensive patterns

Strategy: validation

Validate before calling

// Derive the theme name the same way the server does (filename without extension), then check uniqueness.
const baseName = file.name.replace(/\.css$/i, '');
const themes: SiteThemeDto[] = await api.get('/api/theme');
const taken = new Set(themes.map(t => t.name.toLowerCase()));
if (taken.has(baseName.toLowerCase())) {
  showError('A theme with that name already exists');
  return;
}
await api.uploadTheme(file);

Type guard

function isThemeNameUnique(baseName: string, themes: { name: string }[]): boolean {
  const taken = new Set(themes.map(t => t.name.toLowerCase()));
  return !taken.has(baseName.toLowerCase());
}

Prevention

When it happens

Trigger: POST /api/theme/upload-theme with a .css file whose base name matches an existing theme name, including shipped built-ins (e.g. 'Dark').

Common situations: Re-uploading a theme already installed; uploading a file whose name collides with a built-in or previously-downloaded theme; case-only differences that normalize to the same name.

Related errors


AI-assisted analysis of Kareadita/Kavita@9c3e540000 (2026-08-13). Data as JSON: /api/errors/8e2cae6da33de0c7. Report an issue: GitHub.