makeplane/plane · error · DRFValidationError
invalid_filter_field
invalid_filter_field
Error message
Filtering on field '{field}' is not allowed What it means
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.
Source
Thrown at apps/api/plane/utils/filters/filter_backend.py:122
if not allowed_fields:
# If no FilterSet is configured, reject filtering to avoid unintended exposure # noqa: E501
raise DRFValidationError(
{
"message": ("Filtering is not enabled for this endpoint (missing filterset_class)"),
"code": "filtering_not_enabled",
}
)
# Extract field names from the filter data
fields = self._extract_field_names(filter_data)
# Check if all fields are allowed
for field in fields:
# Field keys must match FilterSet filter names (including any lookups)
# Example: 'sequence_id__gte' should be declared in base_filters
# Special-case __range: require the '<base>__range' filter itself
if field not in allowed_fields:
raise DRFValidationError(
{
"message": f"Filtering on field '{field}' is not allowed",
"code": "invalid_filter_field",
}
)
def _transform_field_name_for_validation(self, field_name):
"""Hook: Transform a field name before validation.
Override this in subclasses to handle special field naming conventions.
Args:
field_name: The original field name from the filter data
Returns:
The transformed field name to validate against the FilterSet
"""
return field_nameView on GitHub (pinned to 1c8a60f858)
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).
Example fix
# before
class IssueFilterSet(BaseFilterSet):
sequence_id = NumberFilter(field_name='sequence_id')
# client sends {"sequence_id__gte": 5}
# after
class IssueFilterSet(BaseFilterSet):
sequence_id = NumberFilter(field_name='sequence_id')
sequence_id__gte = NumberFilter(field_name='sequence_id', lookup_expr='gte') Defensive patterns
Strategy: validation
Validate before calling
ALLOWED = set(view.filterset_class.base_filters.keys())
missing = [f for f in extracted_field_names(filter_data) if f not in ALLOWED]
if missing:
raise ValueError(f'Unsupported filter fields: {missing}. Allowed: {sorted(ALLOWED)}') Type guard
def extract_field_names(node):
fields = []
if isinstance(node, dict):
for k, v in node.items():
if isinstance(k, str) and k.lower() in ('or', 'and'):
for c in v:
fields.extend(extract_field_names(c))
elif isinstance(k, str) and k.lower() == 'not':
fields.extend(extract_field_names(v))
else:
fields.append(k)
return fields Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
AI-assisted analysis of makeplane/plane@1c8a60f858 (2026-08-12).
Data as JSON: /api/errors/bdce5d404d09cbf1.
Report an issue: GitHub.