jamiepine/voicebox · warning · ValueError

Cannot delete the default channel

Error message

Cannot delete the default channel

What it means

Raised as ValueError (HTTP 400) by delete_channel when the channel's is_default=True. The default channel is protected from deletion so an audio output always exists. The guard runs after the lookup but before any DELETE of mappings, so no partial cleanup occurs on rejection. A non-existent channel returns False (surfaced as 404 by the route), not this error.

Source

Thrown at backend/services/channels.py:173

    device_ids = [m.device_id for m in device_mappings]
    
    return AudioChannelResponse(
        id=channel.id,
        name=channel.name,
        is_default=channel.is_default,
        device_ids=device_ids,
        created_at=channel.created_at,
    )


async def delete_channel(channel_id: str, db: Session) -> bool:
    """Delete an audio channel."""
    channel = db.query(DBAudioChannel).filter_by(id=channel_id).first()
    if not channel:
        return False
    
    if channel.is_default:
        raise ValueError("Cannot delete the default channel")
    
    # Delete device mappings
    db.query(DBChannelDeviceMapping).filter_by(channel_id=channel_id).delete()
    
    # Delete profile-channel mappings
    db.query(DBProfileChannelMapping).filter_by(channel_id=channel_id).delete()
    
    # Delete channel
    db.delete(channel)
    db.commit()
    
    return True


async def get_channel_voices(channel_id: str, db: Session) -> List[str]:
    """Get list of profile IDs assigned to a channel."""
    mappings = db.query(DBProfileChannelMapping).filter_by(
        channel_id=channel_id

View on GitHub (pinned to 51f49dea19)

Solutions

  1. Hide/disable the delete action for the channel with is_default=true in the UI.
  2. In bulk operations, filter out default channel ids before issuing DELETEs.
  3. If a different default is needed, seed/migrate a new default rather than deleting this one.

Example fix

// before
for (const id of selectedIds) await api.deleteChannel(id);

// after
for (const id of selectedIds) {
  const ch = channels.find(c => c.id === id);
  if (ch?.is_default) continue;
  await api.deleteChannel(id);
}
Defensive patterns

Strategy: validation

Validate before calling

function isDeletableChannel(ch) {
  return Boolean(ch) && ch.is_default === false;
}
const targets = selected.filter(isDeletableChannel);
if (!targets.length) { notify('Select a non-default channel to delete'); return; }

Type guard

function isRemovableChannel(ch) {
  return Boolean(ch) && ch.is_default === false;
}

Try / catch

try { await api.deleteChannel(id); }
catch (e) {
  if (e.status === 400 && /default channel/.test(e.detail)) notify('Default channel cannot be deleted');
  else if (e.status === 404) { dropChannel(id); }
  else throw e;
}

Prevention

When it happens

Trigger: DELETE /channels/{channel_id} where channel_id is the default channel (is_default=True).

Common situations: User clicks delete on the Default channel; client offers a delete affordance on every channel without filtering is_default; bulk-delete loop includes the default id.

Related errors


AI-assisted analysis of jamiepine/voicebox@51f49dea19 (2026-08-12). Data as JSON: /api/errors/7e0939bc2c9d59c1. Report an issue: GitHub.