passbolt/passbolt_api · error · BadRequestException

The tag id is not valid.

Error message

The tag id is not valid.

What it means

TagsDeleteController::delete() validates the tag id route parameter with Validation::uuid() and throws BadRequestException 'The tag id is not valid.' for non-UUID input, before attempting to load the tag. Separate handling (RecordNotFoundException) covers valid UUIDs that don't exist.

Solutions

  1. Look up the tag's UUID via GET /tags.json and delete using that id
  2. Fix client code to send the UUID field, not the slug
  3. Guard against null/empty ids client-side before issuing DELETE

Example fix

// before
await api.delete(`/tags/${tag.slug}`);
// after
if (!/^[0-9a-f-]{36}$/i.test(tag.id)) throw new Error('Tag id must be a UUID');
await api.delete(`/tags/${tag.id}`);
Defensive patterns

Strategy: validation

Validate before calling

if (!isUuid(tagId)) throw new Error('tagId must be a UUID');

Type guard

const isUuid = (v) => typeof v === 'string' && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(v);

Try / catch

try { await api.delete(`/tags/${tagId}`); } catch (e) { if (e.status === 400) { /* id format wrong — use UUID from /tags.json */ } else throw e; }

Prevention

When it happens

Trigger: DELETE /tags/<id> with a non-UUID id: numeric id, slug string, empty/null id, or malformed identifier.

Common situations: Client passing the tag slug (e.g. 'marketing') instead of the tag UUID; route segment missing so null is passed; using an id from a legacy non-UUID tag store.

Understand the failure class

Background: "invalid id" errors: invalid identifier format — why libraries reject IDs before lookup, and how to fix them — this error's family across 37 libraries.

Related errors


AI-assisted analysis of passbolt/passbolt_api@31c1bbc10f (2026-09-17). Data as JSON: /api/errors/2f266a3026fa6baa. Report an issue: GitHub.

Appendix: source

Thrown at plugins/PassboltEe/Tags/src/Controller/Tags/TagsDeleteController.php:55

     * @inheritDoc
     */
    public function initialize(): void
    {
        parent::initialize();
        $this->Tags = $this->fetchTable('Passbolt/Tags.Tags');
        $this->ResourcesTags = $this->fetchTable('Passbolt/Tags.ResourcesTags');
    }

    /**
     * Tag delete action
     *
     * @param string|null $id Id of the tag to delete
     * @return void
     */
    public function delete(?string $id = null)
    {
        if (!Validation::uuid($id)) {
            throw new BadRequestException(__('The tag id is not valid.'));
        }

        try {
            /** @var \Passbolt\Tags\Model\Entity\Tag $tag */
            $tag = $this->Tags->get($id, contain: ['ResourcesTags']);
        } catch (RecordNotFoundException $e) {
            throw new NotFoundException(__('The tag does not exist.'));
        }

        if ($tag->get('is_shared')) {
            throw new ForbiddenException(__('You do not have the permission to delete shared tags.'));
        }

        if (!$this->isPersonalTagAccessible($tag)) {
            throw new NotFoundException(__('The tag does not exist.'));
        }

        $this->Tags->delete($tag);

View on GitHub (pinned to 31c1bbc10f)