{"record":{"id":"3cb7538fc5c21d7e","repo":"passbolt/passbolt_api","slug":"no-validation-rule-for-filter-0-please-create-one","errorCode":null,"errorMessage":"No validation rule for filter {0}. Please create one.","messagePattern":"No validation rule for filter (.+?)\\. Please create one\\.","errorType":"validation","errorClass":"CakeException","httpStatus":400,"severity":"error","filePath":"src/Controller/Component/QueryStringComponent.php","lineNumber":349,"sourceCode":"                    case 'is-deleted':\n                    case 'expired':\n                    case 'deleted':\n                        self::validateFilterBoolean($values, $filterName);\n                        break;\n                    case 'has-tag':\n                        self::validateFilterString($values, $filterName);\n                        break;\n                    case 'frequency':\n                        self::validateFilterInteger($values, $filterName);\n                        break;\n                    case 'metadata_key_type':\n                        self::validateFilterInList($values, $filterName, ['user_key', 'shared_key']);\n                        break;\n                    default:\n                        // Check if custom filter validators were defined for this filter\n                        if (!isset($filterValidators[$filterName])) {\n                            $msg = __('No validation rule for filter {0}. Please create one.', $filterName);\n                            throw new CakeException($msg);\n                        }\n\n                        if (!call_user_func($filterValidators[$filterName], $values)) {\n                            throw new CakeException(__('Filter {0} is not valid.', $filterName));\n                        }\n\n                        break;\n                }\n            }\n        }\n\n        return true;\n    }\n\n    /**\n     * Check if the filter is a valid string\n     *\n     * @param mixed $value to check","sourceCodeStart":331,"sourceCodeEnd":367,"githubUrl":"https://github.com/passbolt/passbolt_api/blob/31c1bbc10f32808a607fa9bd81891e898779c0bc/src/Controller/Component/QueryStringComponent.php#L331-L367","documentation":"Raised inside QueryStringComponent::validateFilters() (surfaced to clients as 'Invalid filter. No validation rule for filter {name}.') when a filter name is not one of the hard-coded cases AND no custom validator callable was registered for it in $filterValidators. The component intentionally fails closed: unknown filters must be explicitly whitelisted with a validation rule before they can be used.","triggerScenarios":"A request uses ?filter[some-custom-key]=... where some-custom-key is neither in the switch (search, from, created-*, has-*, is-*, modified-after, expired, deleted, has-tag, frequency, metadata_key_type) nor present in the $filterValidators array passed by the controller/plugin.","commonSituations":"A plugin introduced a new filter but forgot to register its validator; a typo in a filter name so it falls through to the default branch; a client sending filters from a different (newer/older) API version; developers testing a new filter endpoint before writing the validator.","solutions":["Check the filter name for typos against the built-in list in validateFilters().","If it is a legitimate new filter, register a validator: pass ['your-filter' => fn($values) => (bool)$values] in the QueryStringComponent config ($filterValidators) of the controller.","If it comes from a plugin, update the plugin or the passbolt version so the filter's validator exists.","If the filter is not needed, remove it from the request.","Confirm client and server API versions match so the client only sends supported filters."],"exampleFix":"// before (controller)\n$this->loadComponent('QueryString', ['filterValidators' => []]);\n// after\n$this->loadComponent('QueryString', ['filterValidators' => [\n    'is-expiring' => function ($values) { return is_scalar($values); },\n]]);","handlingStrategy":"validation","validationCode":"// Server-side guard before relying on a custom filter\n$builtin = ['search','from','created-before','created-after','has-access','has-id','has-managers','has-users','has-groups','has-parent','is-shared-with-group','modified-after','is-active','is-admin','is-favorite','is-owned-by-me','is-shared-with-me','is-shared','is-success','is-deleted','expired','deleted','has-tag','frequency','metadata_key_type'];\n$registered = array_keys($filterValidators); // passed to QueryStringComponent\nif (!in_array($myFilterName, $builtin, true) && !in_array($myFilterName, $registered, true)) {\n    throw new LogicException(\"Filter '$myFilterName' has no validator; register it in filterValidators\");\n}","typeGuard":"function filterHasValidator(string $name, array $filterValidators, array $builtin): bool {\n    return in_array($name, $builtin, true) || array_key_exists($name, $filterValidators);\n}","tryCatchPattern":"try {\n    $result = $api->get('/items.json', ['query' => ['filter' => $filters]]);\n} catch (BadRequestException $e) {\n    if (str_contains($e->getMessage(), 'No validation rule for filter')) {\n        $logger->error('Server does not support filter', ['filter' => array_keys($filters)]);\n        throw new UnsupportedFilterException($e->getMessage(), previous: $e);\n    }\n    throw $e;\n}","preventionTips":["When adding a new filter to a plugin, register its validator in the same change","Client-side: only send filters listed in the API version you target","Add a smoke test hitting the endpoint with every filter your client emits","Check the built-in switch cases in validateFilters() before inventing a custom name","Fail fast in dev: log full BadRequest messages so 'No validation rule' surfaces immediately"],"tags":["filters","validation","query-string","missing-validator","developer-error"],"backgroundTag":"missing-required-config-field","analyzedSha":"31c1bbc10f32808a607fa9bd81891e898779c0bc","analyzedAt":"2026-09-17T00:04:38.960Z","contentChangedAt":"2026-09-17T00:04:38.960Z","schemaVersion":2},"datasetVersion":"2026-09-21T04:17:39.646Z"}