Repository: wagtail/wagtail

Issue:
Bug with removing meta fields from API
<!--
Found a bug? Please fill out the sections below. 👍
-->

### Issue Summary
The `_get_serializer_class` method of the `BaseAPIViewSet` class implements this code snippet which causes some (probably) unexpected errors when removing the meta fields.

```python
        all_fields = body_fields + meta_fields

        # Remove any duplicates
        all_fields = list(OrderedDict.fromkeys(all_fields))

        if not show_details:
            # Remove detail only fields
            for field in cls.detail_only_fields:
                try:
                    all_fields.remove(field)
                except KeyError:
                    pass
```

`all_fields` is converted to a `list` but then a few lines down, we try to handle `KeyError`. The list will never give a `KeyError`, but rather `ValueError: list.remove(x): x not in list`.
This can cause weird behavior when you try to remove meta fields, for instance.


<!--
A summary of the issue.
-->

### Steps to Reproduce

1. Create the following custom `APIViewSet`
```python
class BlogAPIEndpoint(PagesAPIViewSet):
    base_serializer_class = PageSerializer

    body_fields = [
        'title',
    ]

    meta_fields = []
```
2. Register the endpoint with the custom View Set: `api_router.register_endpoint('blog', BlogAPIEndpoint)`
3. Try to call the endpoint to get the results. You will get the error.

Any other relevant information. For example, why do you consider this a bug and what did you expect to happen instead?

-   I have confirmed that this issue can be reproduced as described on a fresh Wagtail project: (yes / no)

### Technical details

-   Python version: `Python 3.8.1`.
-   Django version: `4.1.7`.
-   Wagtail version: `4.1.2`.
-   Browser version: irrelevent.



Discussion:

Resolving patch:
diff --git a/wagtail/api/v2/views.py b/wagtail/api/v2/views.py
--- a/wagtail/api/v2/views.py
+++ b/wagtail/api/v2/views.py
@@ -238,7 +238,7 @@ def _get_serializer_class(
             for field in cls.detail_only_fields:
                 try:
                     all_fields.remove(field)
-                except KeyError:
+                except ValueError:
                     pass
 
         # Get list of configured fields


Test patch:
diff --git a/wagtail/api/v2/tests/test_pages.py b/wagtail/api/v2/tests/test_pages.py
--- a/wagtail/api/v2/tests/test_pages.py
+++ b/wagtail/api/v2/tests/test_pages.py
@@ -1026,6 +1026,11 @@ def test_issue_3967(self):
         response = self.get_response()
         self.assertEqual(response.status_code, 200)
 
+    def test_issue_10411(self):
+        # Bug with removing meta fields from API
+        response = self.client.get(reverse("testapp_api_v2:test_issue_10411:listing"))
+        self.assertEqual(response.status_code, 200)
+
 
 class TestPageListingSearch(WagtailTestUtils, TransactionTestCase):
     fixtures = ["demosite.json"]
diff --git a/wagtail/test/testapp/urls.py b/wagtail/test/testapp/urls.py
--- a/wagtail/test/testapp/urls.py
+++ b/wagtail/test/testapp/urls.py
@@ -1,7 +1,11 @@
 from django.urls import path
 
+from wagtail.api.v2.router import WagtailAPIRouter
 from wagtail.test.testapp import views
 
+api_router = WagtailAPIRouter("testapp_api_v2")
+api_router.register_endpoint("test_issue_10411", views.Test10411ApiViewSet)
+
 urlpatterns = [
     path("bob-only-zone", views.bob_only_zone, name="testapp_bob_only_zone"),
     path("messages/", views.message_test, name="testapp_message_test"),
@@ -14,4 +18,5 @@
         views.TestDeleteView.as_view(),
         name="testapp_generic_delete",
     ),
+    path("api/v2/", api_router.urls),
 ]
diff --git a/wagtail/test/testapp/views.py b/wagtail/test/testapp/views.py
--- a/wagtail/test/testapp/views.py
+++ b/wagtail/test/testapp/views.py
@@ -7,6 +7,7 @@
 from wagtail.admin import messages
 from wagtail.admin.auth import user_passes_test
 from wagtail.admin.views.generic import DeleteView, EditView, IndexView
+from wagtail.api.v2.views import PagesAPIViewSet
 from wagtail.contrib.forms.views import SubmissionsListView
 
 from .models import ModelWithStringTypePrimaryKey
@@ -81,3 +82,7 @@ class TestDeleteView(DeleteView):
     delete_url_name = "testapp_generic_delete"
     success_message = gettext_lazy("User '%(object)s' updated.")
     page_title = gettext_lazy("test delete view")
+
+
+class Test10411ApiViewSet(PagesAPIViewSet):
+    meta_fields = []