t8y2/dbx · error

The built-in exchange '%s' cannot be deleted

Error message

The built-in exchange '%s' cannot be deleted

What it means

Exchanges named 'amq.*' are built-in exchanges predeclared by RabbitMQ and are reserved/deletion-protected by the broker. The driver refuses to delete them (and the default '' exchange) client-side to avoid guaranteed broker failures.

Source

Thrown at agents/drivers/rabbitmq/operations.go:487

	if strings.TrimSpace(name) == "" {
		return "", errors.New("name is required")
	}
	return name, nil
}

func validateExchangeType(exchangeType string) (string, error) {
	if _, ok := exchangeTypes[exchangeType]; !ok {
		return "", fmt.Errorf("Invalid exchange type '%s'. Supported types: direct, fanout, topic, headers", exchangeType)
	}
	return exchangeType, nil
}

func assertExchangeDeletable(name string) error {
	if name == "" {
		return errors.New("The default exchange cannot be deleted")
	}
	if strings.HasPrefix(name, "amq.") {
		return fmt.Errorf("The built-in exchange '%s' cannot be deleted", name)
	}
	return nil
}

func (s *server) listBindings(params jsonObject) (any, error) {
	connection, err := s.requireConnectionConfig(params)
	if err != nil {
		return nil, err
	}
	allVhosts := allVhostsRequested(params)
	bindings, err := managementGetAll(connection, managementListPath(params, connection, "bindings"))
	if err != nil {
		return nil, err
	}
	exchangeFilter := stringOrEmpty(params, "exchange")
	queueFilter := stringOrEmpty(params, "queue")
	result := make([]jsonObject, 0, len(bindings))
	for _, value := range bindings {

View on GitHub (pinned to c0390bff16)

Solutions

  1. Filter out 'amq.'-prefixed and empty-name exchanges from your delete list
  2. Delete only application-created exchanges
  3. Adjust discovery code to skip built-ins when enumerating exchanges

Example fix

// before
for (const e of exchanges) await client.deleteExchange({"name": e.name}) // includes amq.topic
// after
for (const e of exchanges) {
  if (e.name !== "" && !e.name.startsWith("amq.")) await client.deleteExchange({"name": e.name})
}
Defensive patterns

Strategy: validation

Validate before calling

function deletableExchange(name) {
  return typeof name === "string" && name !== "" && !name.startsWith("amq.")
}

Type guard

function isUserExchange(v) {
  return typeof v === "string" && v.length > 0 && !v.startsWith("amq.")
}

Try / catch

try {
  await client.deleteExchange({ name })
} catch (e) {
  if (String(e.message).includes("built-in exchange")) {
    console.warn(`skipping protected exchange ${name}`)
  } else { throw e }
}

Prevention

When it happens

Trigger: Calling deleteExchange with a name starting with 'amq.' (e.g. 'amq.direct', 'amq.topic') or the empty-string default exchange.

Common situations: Listing exchanges from the broker and blindly deleting every entry, including built-ins; cleanup scripts iterating all exchanges without filtering.

Related errors


AI-assisted analysis of t8y2/dbx@c0390bff16 (2026-09-05). Data as JSON: /api/errors/6e211d214898688c. Report an issue: GitHub.