block/buzz · error · IngestError::Rejected

channel is archived

Error message

channel is archived

What it means

The target channel has archived_at set and the incoming event is not a kind:9002 carrying an ["archived", "false"] tag. Archived channels are frozen for mutations — PUT_USER, REMOVE_USER, metadata edits (except the unarchive edit itself), deletes, join/leave requests — so the event is rejected before storage. The only key that opens the gate is a 9002 edit whose tags include archived=false.

Source

Thrown at crates/buzz-relay/src/handlers/side_effects.rs:335

    let channel_id =
        extract_h_tag_channel(event).ok_or_else(|| anyhow::anyhow!("missing or invalid h tag"))?;

    let actor_bytes = event.pubkey.to_bytes().to_vec();

    // Reject mutations on archived channels — except kind:9002 with archived=false
    // (unarchive), which must be allowed through so the channel can be restored.
    let channel = state
        .db
        .get_channel(tenant.community(), channel_id)
        .await
        .map_err(|_| anyhow::anyhow!("channel not found"))?;
    let is_unarchive_request = kind == 9002
        && event.tags.iter().any(|t| {
            let parts = t.as_slice();
            parts.len() >= 2 && parts[0] == "archived" && parts[1] == "false"
        });
    if channel.archived_at.is_some() && !is_unarchive_request {
        return Err(anyhow::anyhow!("channel is archived"));
    }

    match kind {
        9000 => {
            // An absent role tag means "no role change requested": for an existing
            // member that preserves the role they already hold, and only defaults
            // to Member for a genuinely new member. Defaulting unconditionally to
            // Member made a bare self-targeted PUT_USER silently demote an owner.
            let role_str = extract_tag_value(event, "role");
            let requested_role = match role_str {
                Some(ref s) => match s.parse::<buzz_db::channel::MemberRole>() {
                    Ok(r) => Some(r),
                    Err(_) => return Err(anyhow::anyhow!("invalid role: {s}")),
                },
                None => None,
            };

            let members = state.db.get_members(tenant.community(), channel_id).await?;

View on GitHub (pinned to f956e6fe06)

Solutions

  1. Unarchive first: publish kind 9002 with tags including ["archived", "false"], then retry the original mutation
  2. Or point the operation at a live channel instead of the archived one
  3. Surface channel.archived_at in the client UI and block admin actions on archived channels preemptively
  4. Check the 9002 unarchive tag is exactly ["archived", "false"] — parts[0]=="archived" && parts[1]=="false"

Example fix

// before: mutation on archived channel → "channel is archived"
EventBuilder::new(Kind::from(9000), "", [/* p, role, h */])

// after: unarchive, then mutate
EventBuilder::new(Kind::from(9002), "", [
    Tag::custom(TagKind::Custom("h"), vec![channel_uuid]),
    Tag::custom(TagKind::Custom("archived"), vec!["false"]),
]).to_event(&owner_keys).await?;
EventBuilder::new(Kind::from(9000), "", [/* p, role, h */])
Defensive patterns

Strategy: validation

Validate before calling

// Check archived state client-side before issuing admin events
let ch = client.get_channel(community, channel_uuid).await?;
let unarchive = kind == 9002 && event_has_tag(&event.tags, "archived", "false");
anyhow::ensure!(
    ch.archived_at.is_none() || unarchive,
    "channel is archived — send 9002 with [\"archived\",\"false\"] first"
);

Type guard

const isUnarchive = (kind: number, tags: string[][]): boolean =>
  kind === 9002 && tags.some(t => t[0] === "archived" && t[1] === "false");

Try / catch

match validate_admin_event(&tenant, kind, &event, &state).await {
    Err(e) if e.to_string().contains("channel is archived") => {
        // Deterministic: send the 9002 unarchive edit (with archived=false), then replay the mutation once
        unarchive_then_retry(channel_uuid, event).await
    }
    other => other,
}

Prevention

When it happens

Trigger: Issuing kind 9000/9001/9005/9008/9021/9022 against a channel that was archived earlier; a 9002 edit that changes name/topic without also including archived=false on an archived channel; an unarchive attempt sent as a different kind or with the tag misspelled (e.g. ["unarchive","true"]); a user re-joining an old archived channel via 9021.

Common situations: Team archives a project channel, later automation still posts membership updates to it; unarchive event built with the wrong tag value ('false' vs false, or 0); clients not surfacing archived state so users retry joins repeatedly.

Related errors


AI-assisted analysis of block/buzz@f956e6fe06 (2026-08-16). Data as JSON: /api/errors/fc7eca308c80685e. Report an issue: GitHub.