{"record":{"id":"57122fedcf8816ad","repo":"makeplane/plane","slug":"filterset-missing","errorCode":"filterset_missing","errorMessage":"Filtering requires a filterset_class to be defined on the view","messagePattern":"Filtering requires a filterset_class to be defined on the view","errorType":"validation","errorClass":"DRFValidationError","httpStatus":400,"severity":"error","filePath":"apps/api/plane/utils/filters/filter_backend.py","lineNumber":250,"sourceCode":"        \"\"\"\n        return leaf_conditions\n\n    def _build_leaf_q(self, leaf_conditions, view, queryset):\n        \"\"\"Build a Q object from leaf filter conditions using the view's FilterSet.\n\n        We serialize the leaf dict into a QueryDict and let the view's\n        filterset_class perform validation and build a combined Q object\n        from all the field filters.\n\n        Returns a Q object representing all the field conditions in the leaf.\n        \"\"\"\n        if not leaf_conditions:\n            return Q()\n\n        # Get the filterset class from the view\n        filterset_class = getattr(view, \"filterset_class\", None)\n        if not filterset_class:\n            raise DRFValidationError(\n                {\n                    \"message\": (\"Filtering requires a filterset_class to be defined on the view\"),\n                    \"code\": \"filterset_missing\",\n                }\n            )\n\n        # Apply preprocessing hook\n        processed_conditions = self._preprocess_leaf_conditions(leaf_conditions, view, queryset)\n\n        # Build a QueryDict from the leaf conditions\n        qd = QueryDict(mutable=True)\n        for key, value in processed_conditions.items():\n            # Default serialization to string; QueryDict expects strings\n            if isinstance(value, list):\n                # Repeat key for list values (e.g., __in)\n                qd.setlist(key, [str(v) for v in value])\n            else:\n                qd[key] = \"\" if value is None else str(value)","sourceCodeStart":232,"sourceCodeEnd":268,"githubUrl":"https://github.com/makeplane/plane/blob/1c8a60f858d8472aa56e29994ec1c7926da2c6ce/apps/api/plane/utils/filters/filter_backend.py#L232-L268","documentation":"Raised by _build_leaf_q when the view has no filterset_class at the moment a leaf condition is turned into a Q object. Conceptually a sibling of filtering_not_enabled (122) but emitted later in the pipeline, after structural validation has already passed — meaning the view allowed field names through _validate_fields but lost the filterset by the time it built the query.","triggerScenarios":"A subclass overrides _validate_fields to skip the allowlist check, or mutates view.filterset_class to None mid-request; in normal flow this is unreachable because _validate_fields already raised filtering_not_enabled. Most likely a custom subclass that bypasses the standard validation path.","commonSituations":"Custom ComplexFilterBackend subclass that overrides _validate_fields or _evaluate_node without preserving the filterset_class guard; concurrent mutation of view attributes during a test; mock frameworks that strip attributes off the view.","solutions":["Ensure filterset_class is set on the view before any filter request (same fix as filtering_not_enabled).","If you subclass ComplexFilterBackend, do not bypass _validate_fields.","In tests, set view.filterset_class explicitly rather than mocking the view wholesale."],"exampleFix":"# before\nclass MyBackend(ComplexFilterBackend):\n    def _validate_fields(self, data, view):\n        pass  # skip allowlist\n\n# after\nclass MyBackend(ComplexFilterBackend):\n    # keep default _validate_fields; just declare filterset_class on the view\n    pass","handlingStrategy":"validation","validationCode":"if not getattr(view, 'filterset_class', None):\n    raise RuntimeError('View %r lacks filterset_class; ComplexFilterBackend cannot build a Q.' % type(view).__name__)","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Do not override _validate_fields to skip the allowlist check.","Keep filterset_class as a class-level attribute so getattr cannot return None mid-request."],"tags":["filters","config","invariant","api"],"backgroundTag":null,"analyzedSha":"1c8a60f858d8472aa56e29994ec1c7926da2c6ce","analyzedAt":"2026-08-12T14:44:31.636Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}