BookStackApp/BookStack · error · NotifyException

Only top-level comments can be archived.

Error message

Only top-level comments can be archived.

What it means

CommentRepo::archive() refuses to archive a comment that has a parent_id, i.e. a nested reply. In this library only top-level comments (parent_id null) support the archived state; replies are managed implicitly through their parent. It throws NotifyException with HTTP status 400 so the client sees a user-friendly message.

Source

Thrown at app/Activity/CommentRepo.php:102

    public function update(Comment $comment, string $html): Comment
    {
        $comment->updated_by = user()->id;
        $comment->html = HtmlDescriptionFilter::filterFromString($html);
        $comment->save();

        ActivityService::add(ActivityType::COMMENT_UPDATE, $comment);

        return $comment;
    }


    /**
     * Archive an existing comment.
     */
    public function archive(Comment $comment, bool $log = true): Comment
    {
        if ($comment->parent_id) {
            throw new NotifyException('Only top-level comments can be archived.', '/', 400);
        }

        $comment->archived = true;
        $comment->save();

        if ($log) {
            ActivityService::add(ActivityType::COMMENT_UPDATE, $comment);
        }

        return $comment;
    }

    /**
     * Un-archive an existing comment.
     */
    public function unarchive(Comment $comment, bool $log = true): Comment
    {
        if ($comment->parent_id) {

View on GitHub (pinned to 18f8469a1c)

Solutions

  1. Check $comment->parent_id is null before calling archive(); skip or handle replies separately.
  2. If the goal is to remove a reply, delete it (or archive its top-level parent) instead of archiving it directly.
  3. Catch NotifyException around the call to surface the 400 message to the user.

Example fix

// before
foreach ($comments as $comment) {
    $commentRepo->archive($comment);
}
// after
foreach ($comments as $comment) {
    if ($comment->parent_id === null) {
        $commentRepo->archive($comment);
    }
}
Defensive patterns

Strategy: validation

Validate before calling

if ($comment->parent_id !== null) {
    throw new InvalidArgumentException('Cannot archive a reply; only top-level comments can be archived.');
}
$commentRepo->archive($comment);

Type guard

function isTopLevelComment($comment): bool {
    return $comment instanceof Comment && $comment->parent_id === null;
}

Try / catch

try {
    $commentRepo->archive($comment);
} catch (NotifyException $e) {
    // handle 400: only top-level comments archivable
}

Prevention

When it happens

Trigger: Calling CommentRepo->archive($comment) (or the API/controller endpoint that reaches it) with a Comment model whose parent_id is set — i.e. a reply to another comment.

Common situations: Bulk-archiving scripts or moderation tools that iterate over a comment list without filtering out replies; passing a comment id obtained from a nested/reply endpoint; assuming all comments are archivable after a version that introduced archiving for top-level comments only.

Related errors


AI-assisted analysis of BookStackApp/BookStack@18f8469a1c (2026-09-02). Data as JSON: /api/errors/2bcceacd170c8628. Report an issue: GitHub.