BookStackApp/BookStack · error · ZipImportException

Selected parent is not a book or chapter

Error message

Selected parent is not a book or chapter

What it means

ImportRepo::runImport supports optional parent context for page and chapter imports. If a parent identifier is supplied but resolves to something other than a Book or Chapter, it throws ZipImportException(['Selected parent is not a book or chapter']). This validates import targets before the DB transaction starts.

Source

Thrown at app/Exports/ImportRepo.php:125

        $import->path = $path;
        $import->save();

        Activity::add(ActivityType::IMPORT_CREATE, $import);

        return $import;
    }

    /**
     * @throws ZipImportException
     */
    public function runImport(Import $import, ?string $parent = null): Entity
    {
        $parentModel = null;
        if ($import->type === 'page' || $import->type === 'chapter') {
            $parentModel = $parent ? $this->entityQueries->findVisibleByStringIdentifier($parent) : null;
            if ($parentModel && !($parentModel instanceof Book || $parentModel instanceof Chapter)) {
                throw new ZipImportException(['Selected parent is not a book or chapter']);
            }
        }

        DB::beginTransaction();
        try {
            $model = $this->importer->run($import, $parentModel);
        } catch (ZipImportException $e) {
            DB::rollBack();
            $this->importer->revertStoredFiles();
            throw $e;
        }

        DB::commit();
        $this->deleteImport($import);
        Activity::add(ActivityType::IMPORT_RUN, $import);

        return $model;
    }

View on GitHub (pinned to 18f8469a1c)

Solutions

  1. Pass a Book or Chapter ID/slug as the parent (or omit $parent entirely to import at root).
  2. Resolve and type-check the parent before calling runImport.
  3. Ensure the importing user has view permission on the target parent so it resolves.
  4. Catch ZipImportException and read its message list to surface the validation failure to the user.

Example fix

// before
$importRepo->runImport($import, $shelfSlug); // shelf is invalid
// after
$parent = $entityQueries->findVisibleByStringIdentifier($parentId);
if ($parent instanceof Book || $parent instanceof Chapter) {
    $importRepo->runImport($import, $parentId);
} else {
    $importRepo->runImport($import); // import to root
}
Defensive patterns

Strategy: validation

Validate before calling

$parent = $entityQueries->findVisibleByStringIdentifier($parentId);
if ($parent && !($parent instanceof \BookStack\Entities\Book || $parent instanceof \BookStack\Entities\Chapter)) {
    throw new \InvalidArgumentException('Import parent must be a Book or Chapter');
}

Type guard

function validImportParent(?object $e): bool {
    return $e === null || $e instanceof \BookStack\Entities\Book || $e instanceof \BookStack\Entities\Chapter;
}

Try / catch

try {
    $importRepo->runImport($import, $parentId);
} catch (\BookStack\Exceptions\ZipImportException $e) {
    return back()->withErrors(['parent' => implode(', ', $e->getMessages())]);
}

Prevention

When it happens

Trigger: runImport($import, $parent) where $parent is a Shelf's or Page's ID/slug, a nonexistent identifier, or an entity the importing user cannot view (findVisibleByStringIdentifier returns null so only a truthy non-Book/Chapter triggers... a null parent is treated as no parent).

Common situations: Automated import pipelines passing shelf IDs as parents; IDs from a staging instance reused in production; slugs that collide with non-book entities; users lacking visibility of the intended parent so lookup yields null and content imports to root instead.

Related errors


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