#11338·wagtail

Bulk loss of redirects after overriding `Page.get_url_parts`

Author: alxbridgeCreated Dec 13, 2023Updated Sep 17, 2026
Labelstype:Bugcomponent:Redirectscomponent:Page

Issue Summary

For models inheriting from Page, overriding the get_url_parts method can, in certain circumstances, result in bulk deletion of Redirects.

Steps to Reproduce

  1. Implement a model inheriting from Page, overriding get_url_parts in such a way that the returned path value doesn't make use of the item's slug.
  2. Create a draft item using this model
  3. Edit the draft item to update its slug
  4. Publish the item

Result: All Redirects which are flagged as automatically_created are deleted

This example model is sufficient to trigger the issue:

python
class DemonstrationPage(Page):
    def get_url_parts(self, request: "HttpRequest" = None) -> tuple[int, str, str]:
        """
        Overrides `Page.get_url_parts()` so that the page's path uses its ID instead of its slug.
        """
        site_id, root_url, path = self.get_parent().get_url_parts(request)
        return site_id, root_url, f"{path}{self.id}/"
  • I have confirmed that this issue can be reproduced as described on a fresh Wagtail project: yes

Technical details

This issue occurs due to a combination of the overridden get_url_parts method and handling inside wagtail.contrib.redirects.signal_handlers.

  • Changes to the item's slug result in a call to the autocreate_redirects_on_slug_change function (which handles the page_slug_changed signal).
  • When the item's slug isn't used in determining its path, this function doesn't add any redirects to the batch for processing (as the item's path hasn't changed).
  • The BatchRedirectCreator.pre_process method attempts to delete any existing redirects which clash with items in the batch:
python
    def pre_process(self):
        # delete any existing automatically-created redirects that might clash
        # with the items in `self.items`
        clashes_q = Q()
        for item in self.items:
            clashes_q |= Q(old_path=item.old_path, site_id=item.site_id)
        Redirect.objects.filter(automatically_created=True).filter(clashes_q).delete()
  • Since there's nothing in self.items, the list of Redirects isn't filtered down further, resulting in deletion of all those set as automatically_created=True.

Possible fixes

  1. The most immediate fix would be to add a check to BatchRedirectCreator.pre_process, to ensure that self.items is populated before proceeding. A similar check already exists in BatchCreator._do_processing for the actual processing of batch items.
  2. Check batch.items before the call to batch.process() in the create_redirects function.
  3. We could move the check from BatchCreator._do_processing to the main BatchCreator.process method, so that no processing action occurs unless a batch contains items. However, I'm not certain whether this would be desirable, and I've not checked whether it might have any unexpected knock-on effects.