Skip to content

Concurrent requests can create duplicates when reordering pages #7964

@aeyoa

Description

@aeyoa

hello dear Kirby team! first of all thank you for your beautiful product. been using Kirby since v2 and very happy with it. for last few years I switched to using Kirby headless as an panel + API for next.js. it works really great overall but a small bug annoys me (sometimes) for a couple of last years. I searched through other issues and this one #2863 looks very similar and its marked as closed. but I'm still catching this issue sometimes

The issue

Sometimes when user of admin panel rearranges pages inside a folder these pages are getting duplicate. here are a few screenshots how it looks:

Image
valid folders contain product.txt and some files. these folders are just empty folders with default.txt template

Image
sometimes duplicates are getting created with the correct template (model.txt in that case)

I have a few more screenshots like these but they are mostly the same: ghost pages without any content with duplicate numbers. that usually leads to panel not showing real (original) pages in panel. usually I just fix this issue manually and it occurs not really often. also i started to use separate order field of type: pages instead of relying on built-in sorting.

today this happened again and I decided to investigate the issue a little with claude. its idea is that the duplication happens because of concurrent API calls and reordering triggered from panel. I'm not competent enough to check if this point is valid but it look reasonable to me. below I'll add the summary made by claude code. and I'll be happy to answer more questions about this issue. here it goes:

Description

When a page reorder/sort operation in the Panel runs concurrently with another PHP process that writes to the same pages (e.g. $page->update() from an API route, cron sync, or plugin), ghost duplicate page directories appear on disk.

Observed on: Kirby 5 (affects v4 too). ~85 listed siblings, shared hosting.

What we observe

After sorting pages in the Panel while a sync script was running $page->update() on the same pages:

79_my-page/          ← original, NOT moved, full content (product.txt + images)
80_my-page/          ← ghost, only default.txt (wrong template), no images
80_other-page/       ← a different page that legitimately has num 80
  • The original page stays at its old sort number with full content — Dir::move apparently did not execute
  • A ghost directory appears at the new sort number with just a default.txt file
  • 5 consecutive pages were affected at once, all ghosts created in the same second
  • The ghost has default.txt instead of product.txt, suggesting the template was resolved via Dir::inventory() fallback ('default') rather than from the actual content file

Likely cause

There is no concurrency protection around sort operations. resortSiblingsAfterListing() renames multiple page directories sequentially via Dir::move, while page objects in other PHP processes still hold cached root paths. When those processes call $page->update()save()F::write(), the write auto-creates missing directories via Dir::make(), producing ghost duplicates.

In our case, the concurrent process was a 1C product sync API route calling $page->update($data) on the same pages being sorted. But any concurrent write operation (plugin, Panel auto-save, cron) could trigger the same issue.

The exact interplay between the sort and concurrent write that leads to the ghost appearing at the new number (while the original stays at the old number) is not fully clear — but the root problem is the lack of any locking or staleness detection when multiple processes operate on the same pages simultaneously.

Suggested fix

Some form of filesystem-level locking around sort operations, or a staleness check in the write path that prevents F::write() from auto-creating page directories that were moved by another process.

P.S.

again, I'm not sure about the cause of this issue but the issue itself is 100% real (although rare and difficult to reproduce). sorry that I can't technically investigate any further. hope I can help make our beloved Kirby a little more robust :)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions