makeplane/plane · error · DRFValidationError

invalid_filterset

invalid_filterset

Error message

Invalid filter parameters

What it means

Raised by _build_leaf_q when filterset_class(data=..., queryset=...).is_valid() returns False. The underlying django_filters FilterSet runs its own per-field validation (range ordering, choice constraints, type coercion), and the errors dict is forwarded through translate_validation and embedded in the exception under 'errors'.

Source

Thrown at apps/api/plane/utils/filters/filter_backend.py:279

        qd = QueryDict(mutable=True)
        for key, value in processed_conditions.items():
            # Default serialization to string; QueryDict expects strings
            if isinstance(value, list):
                # Repeat key for list values (e.g., __in)
                qd.setlist(key, [str(v) for v in value])
            else:
                qd[key] = "" if value is None else str(value)

        qd = qd.copy()
        qd._mutable = False

        # Instantiate the filterset with the actual queryset
        # Custom filter methods may need access to the queryset for filtering
        fs = filterset_class(data=qd, queryset=queryset)

        if not fs.is_valid():
            ve = translate_validation(fs.errors)
            raise DRFValidationError(
                {
                    "message": "Invalid filter parameters",
                    "code": "invalid_filterset",
                    "errors": ve.detail,
                }
            )

        # Build and return the combined Q object
        if not hasattr(fs, "build_combined_q"):
            raise DRFValidationError(
                {
                    "message": ("FilterSet must have build_combined_q method for complex filtering"),
                    "code": "missing_build_combined_q",
                }
            )

        return fs.build_combined_q()

View on GitHub (pinned to 1c8a60f858)

Solutions

  1. Inspect the 'errors' field of the response — it carries per-field detail from the filterset.
  2. Sort __range bounds so the smaller value comes first: [5, 10] not [10, 5].
  3. Confirm the value is a member of the ChoiceFilter choices or a valid PK for ModelChoiceFilter.
  4. If the value should be permitted, widen the filterset's choices or field definition.

Example fix

// before
?filters={"sequence_id__range":[10,5]}
// after
?filters={"sequence_id__range":[5,10]}
Defensive patterns

Strategy: try-catch

Validate before calling

fs = view.filterset_class(data=qd, queryset=qs)
if not fs.is_valid():
    # fs.errors is a dict of per-field error lists
    raise ValueError(f'filterset invalid: {fs.errors}')

Try / catch

try:
    return backend.filter_queryset(request, qs, view)
except DRFValidationError as e:
    detail = e.detail
    if isinstance(detail, dict) and detail.get('code') == 'invalid_filterset':
        return Response({'field_errors': detail.get('errors')}, status=400)
    raise

Prevention

When it happens

Trigger: GET .../?filters={"sequence_id__range":[10,5]} (range where start>end); GET .../?filters={"state":"deleted"} when state is a ChoiceFilter that does not include 'deleted'; non-numeric string sent to a NumberFilter.

Common situations: Semantic mismatch between client enum and server ChoiceFilter; reversed __range bounds; sending an ID that does not exist for a ModelChoiceFilter; type drift after a field changed from CharFilter to NumberFilter.

Related errors


AI-assisted analysis of makeplane/plane@1c8a60f858 (2026-08-12). Data as JSON: /api/errors/4c7b2de34773ae76. Report an issue: GitHub.