{"record":{"id":"bdce5d404d09cbf1","repo":"makeplane/plane","slug":"invalid-filter-field","errorCode":"invalid_filter_field","errorMessage":"Filtering on field '{field}' is not allowed","messagePattern":"Filtering on field '(.+?)' is not allowed","errorType":"validation","errorClass":"DRFValidationError","httpStatus":400,"severity":"error","filePath":"apps/api/plane/utils/filters/filter_backend.py","lineNumber":122,"sourceCode":"        if not allowed_fields:\n            # If no FilterSet is configured, reject filtering to avoid unintended exposure # noqa: E501\n            raise DRFValidationError(\n                {\n                    \"message\": (\"Filtering is not enabled for this endpoint (missing filterset_class)\"),\n                    \"code\": \"filtering_not_enabled\",\n                }\n            )\n\n        # Extract field names from the filter data\n        fields = self._extract_field_names(filter_data)\n\n        # Check if all fields are allowed\n        for field in fields:\n            # Field keys must match FilterSet filter names (including any lookups)\n            # Example: 'sequence_id__gte' should be declared in base_filters\n            # Special-case __range: require the '<base>__range' filter itself\n            if field not in allowed_fields:\n                raise DRFValidationError(\n                    {\n                        \"message\": f\"Filtering on field '{field}' is not allowed\",\n                        \"code\": \"invalid_filter_field\",\n                    }\n                )\n\n    def _transform_field_name_for_validation(self, field_name):\n        \"\"\"Hook: Transform a field name before validation.\n\n        Override this in subclasses to handle special field naming conventions.\n\n        Args:\n            field_name: The original field name from the filter data\n\n        Returns:\n            The transformed field name to validate against the FilterSet\n        \"\"\"\n        return field_name","sourceCodeStart":104,"sourceCodeEnd":140,"githubUrl":"https://github.com/makeplane/plane/blob/1c8a60f858d8472aa56e29994ec1c7926da2c6ce/apps/api/plane/utils/filters/filter_backend.py#L104-L140","documentation":"Raised by _validate_fields when a field name extracted from the filter tree is not present in the view's filterset_class.base_filters keys. base_filters is the declared allowlist of filters (including lookup variants like sequence_id__gte), so this fires on typos, renamed fields, or attempts to filter on a field the view never exposed.","triggerScenarios":"GET /api/workspaces/<slug>/issues/?filters={\"stat\":\"open\"} (typo); GET .../?filters={\"assigneed__in\":[...]} (extra d); GET .../?filters={\"archived_at__isnull\":true} on a view whose filterset does not declare that lookup.","commonSituations":"Frontend references a field that was renamed in a backend migration; client expects a filter the EE/CE split does not expose; using a Django ORM lookup suffix (__gte, __in, __range) whose filter is not explicitly declared on the filterset.","solutions":["Inspect the view's filterset_class.declared_filters and base_filters to confirm the exact allowed key names.","Add the missing filter to the filterset, e.g. `archived_at__isnull = django_filters.BooleanFilter(field_name='archived_at', lookup_expr='isnull')`.","Fix the typo / use the correct lookup suffix; remember lookups must be declared individually (sequence_id__gte is a separate key from sequence_id)."],"exampleFix":"# before\nclass IssueFilterSet(BaseFilterSet):\n    sequence_id = NumberFilter(field_name='sequence_id')\n# client sends {\"sequence_id__gte\": 5}\n\n# after\nclass IssueFilterSet(BaseFilterSet):\n    sequence_id = NumberFilter(field_name='sequence_id')\n    sequence_id__gte = NumberFilter(field_name='sequence_id', lookup_expr='gte')","handlingStrategy":"validation","validationCode":"ALLOWED = set(view.filterset_class.base_filters.keys())\nmissing = [f for f in extracted_field_names(filter_data) if f not in ALLOWED]\nif missing:\n    raise ValueError(f'Unsupported filter fields: {missing}. Allowed: {sorted(ALLOWED)}')","typeGuard":"def extract_field_names(node):\n    fields = []\n    if isinstance(node, dict):\n        for k, v in node.items():\n            if isinstance(k, str) and k.lower() in ('or', 'and'):\n                for c in v:\n                    fields.extend(extract_field_names(c))\n            elif isinstance(k, str) and k.lower() == 'not':\n                fields.extend(extract_field_names(v))\n            else:\n                fields.append(k)\n    return fields","tryCatchPattern":null,"preventionTips":["Expose filterset_class.base_filters to the frontend (e.g. via an OPTIONS response) so the client knows the allowlist.","Declare every lookup suffix you intend to support (__gte, __in, __range) explicitly on the filterset."],"tags":["filters","allowlist","validation","api"],"backgroundTag":null,"analyzedSha":"1c8a60f858d8472aa56e29994ec1c7926da2c6ce","analyzedAt":"2026-08-12T14:44:31.636Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}