ArchiveBox/ArchiveBox · error · ValueError

Tag "{existing.name}" already exists

Error message

Tag "{existing.name}" already exists

What it means

rename_tag raises ValueError when another tag (different primary key) already exists with a case-insensitively equal name after normalization. This guards Tag.name uniqueness when renaming, since Django's unique constraint alone can miss case-variant duplicates. It fires before any rename is applied, so state is unchanged.

Source

Thrown at archivebox/core/tag_util.py:173

    existing = Tag.objects.filter(name__iexact=normalized_name).first()
    if existing:
        return existing, False

    tag = Tag.objects.create(
        name=normalized_name,
        created_by=created_by,
    )
    return tag, True


def rename_tag(tag: Tag, name: str) -> Tag:
    normalized_name = normalize_tag_name(name)
    if not normalized_name:
        raise ValueError("Tag name is required")

    existing = Tag.objects.filter(name__iexact=normalized_name).exclude(pk=tag.pk).first()
    if existing:
        raise ValueError(f'Tag "{existing.name}" already exists')

    if tag.name != normalized_name:
        tag.name = normalized_name
        tag.save()
    return tag


def delete_tag(tag: Tag) -> tuple[int, dict[str, int]]:
    return tag.delete()


def export_tag_urls(tag: Tag) -> str:
    urls = tag.snapshot_set.order_by("-downloaded_at", "-created_at", "-pk").values_list("url", flat=True)
    return "\n".join(urls)


def export_tag_snapshots_jsonl(tag: Tag) -> str:
    snapshots = tag.snapshot_set.order_by("-downloaded_at", "-created_at", "-pk").prefetch_related("tags")

View on GitHub (pinned to 74564b2822)

Solutions

  1. Choose a different target name that does not collide (case-insensitively) with any existing tag.
  2. Merge the two tags first (move all Snapshot-tag links onto the existing tag, delete the duplicate), then rename if still needed.
  3. Query Tag.objects.filter(name__iexact=normalize_tag_name(new_name)).exclude(pk=tag.pk) before calling rename_tag to pre-check collisions.

Example fix

// before
rename_tag(tag, "News")  # ValueError: Tag "news" already exists
// after
existing = Tag.objects.get(name__iexact="news")
for snap in tag.snapshot_set.all():
    existing.snapshot_set.add(snap)
tag.snapshot_set.clear()
tag.delete()
Defensive patterns

Strategy: validation

Validate before calling

from archivebox.core.tag_util import normalize_tag_name
new_name = normalize_tag_name(candidate)
collision = Tag.objects.filter(name__iexact=new_name).exclude(pk=tag.pk).exists()
if not new_name or collision:
    # pick another name or merge instead of renaming
    ...

Try / catch

try:
    rename_tag(tag, new_name)
except ValueError as e:
    if "already exists" in str(e):
        merge_into_existing(tag, Tag.objects.get(name__iexact=normalize_tag_name(new_name)))
    else:
        raise

Prevention

When it happens

Trigger: Calling rename_tag(tag, name) where normalize_tag_name(name) matches (case-insensitively) the name of a different Tag row, e.g. renaming 'News' to 'news' when a tag 'news' already exists.

Common situations: Users rename a tag to a capitalized/lowercased variant of an existing tag; case-insensitive DB collations or prior imports created near-duplicate tags; admin/API flows allow overlapping names.

Related errors


AI-assisted analysis of ArchiveBox/ArchiveBox@74564b2822 (2026-08-28). Data as JSON: /api/errors/9e6482986fce6705. Report an issue: GitHub.