{"record":{"id":"83a2d48289a26469","repo":"passbolt/passbolt_api","slug":"invalid-query-string-the-filter-parameter-should-be-an-array","errorCode":null,"errorMessage":"Invalid query string. The filter parameter should be an array.","messagePattern":"Invalid query string\\. The filter parameter should be an array\\.","errorType":"http","errorClass":"BadRequestException","httpStatus":400,"severity":"error","filePath":"src/Controller/Component/QueryStringComponent.php","lineNumber":113,"sourceCode":"     * @param array $query original query string items\n     * @return array modified query\n     */\n    public static function normalizeQueryItems(array $query): array\n    {\n        // order should always be an array even when one value is provided\n        // this is deprecated, order is now handled by the ApiPaginationComponent\n        if (isset($query['order']) && !is_array($query['order'])) {\n            $query['order'] = [$query['order']];\n        }\n\n        // filters with is-* means we are expecting a boolean\n        // we accept 'TRUE', 'true', '1' as true and the rest is set to false\n        if (isset($query['filter'])) {\n            if ($query['filter'] == '[]') {\n                $query['filter'] = [];\n            }\n            if (!is_array($query['filter'])) {\n                throw new BadRequestException(__('Invalid query string. The filter parameter should be an array.'));\n            }\n            foreach ($query['filter'] as $filterName => $filter) {\n                if (!is_string($filterName)) {\n                    continue;\n                }\n                if (in_array($filterName, self::mustBeArrayFilters())) {\n                    // these should always be an array\n                    $query['filter'][$filterName] = $filter = (array)$query['filter'][$filterName];\n                }\n                $booleanFilters = ['deleted', 'expired'];\n                if (substr($filterName, 0, 3) === 'is-' || in_array($filterName, $booleanFilters)) {\n                    $query['filter'][$filterName] = self::normalizeBoolean($filter);\n                } elseif ($filterName === 'has-parent') {\n                    foreach ($query['filter']['has-parent'] as $i => $parentId) {\n                        if ($parentId === 'false' || $parentId === '0') {\n                            $query['filter']['has-parent'][$i] = false;\n                        }\n                    }","sourceCodeStart":95,"sourceCodeEnd":131,"githubUrl":"https://github.com/passbolt/passbolt_api/blob/31c1bbc10f32808a607fa9bd81891e898779c0bc/src/Controller/Component/QueryStringComponent.php#L95-L131","documentation":"QueryStringComponent::normalizeQueryItems() requires the `filter` query parameter to be an array. If `filter` is present but not an array (and not the literal '[]', which is normalized to []), it throws BadRequestException('Invalid query string. The filter parameter should be an array.') producing a 400.","triggerScenarios":"GET collection endpoints with `?filter=<scalar>` — e.g. `?filter=has-managers` (bare string instead of `filter[]=has-managers`), a JSON object instead of array syntax, or a filter supplied with mismatched bracket syntax so PHP parses it as a string.","commonSituations":"Hand-built query strings missing the `[]` in filter names; clients serializing filters as JSON (`{\"...\":...}`) instead of PHP/bracket array syntax; URL encoding stripping the brackets; copying examples from other APIs with different filter conventions.","solutions":["Send filters using array syntax: `?filter[]=has-managers` or named filters `?filter[is-deleted]=true`.","To request no filters, either omit the parameter entirely or send the literal `?filter=[]` (explicitly normalized to an empty array).","Check URL encoding — `[]` must survive encoding (`filter%5B%5D=...`); fix client-side query builders that encode brackets incorrectly.","If using a JSON body style, stop: this API expects bracket-array query strings, not JSON in query parameters."],"exampleFix":"// before — scalar filter, rejected\nGET /users.json?filter=has-access\n// after — array syntax\nGET /users.json?filter[]=has-access\n// or multiple:\nGET /users.json?filter[]=is-admin&filter[]=active","handlingStrategy":"type-guard","validationCode":"// ensure filters serialize as array query params\nfunction buildFilterParams(filters) {\n  const p = new URLSearchParams();\n  for (const f of filters) p.append('filter[]', f); // emits filter[]=value\n  return p;\n}","typeGuard":"const isFilterArray = (v) => v === undefined || v === null || Array.isArray(v) || (typeof v === 'object' && v !== null); // never a bare string/number","tryCatchPattern":"try {\n  return await api.get('/users.json', { params });\n} catch (e) {\n  if (e.response?.status === 400 && /filter parameter should be an array/.test(e.response?.data?.message ?? '')) {\n    throw new Error('Send filters as filter[]=value, not filter=value');\n  }\n  throw e;\n}","preventionTips":["Always emit `filter[]=` (URL-encoded `filter%5B%5D=`) in query builders, never a bare `filter=`.","Do not put JSON objects into query string values for filters.","Add a unit test asserting serialized filter parameters keep bracket syntax after URL encoding.","When no filters apply, omit the parameter rather than sending a scalar."],"tags":["query-string","filter","http-400","bad-request"],"backgroundTag":"invalid-query-parameter","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"}