{"record":{"id":"a9ade49cad3fb9ac","repo":"passbolt/passbolt_api","slug":"invalid-query-string-the-contain-parameter-should-be-an","errorCode":null,"errorMessage":"Invalid query string. The contain parameter should be an array.","messagePattern":"Invalid query string\\. The contain parameter should be an array\\.","errorType":"http","errorClass":"BadRequestException","httpStatus":400,"severity":"error","filePath":"src/Controller/Component/QueryStringComponent.php","lineNumber":146,"sourceCode":"                        if ($parentId === 'false' || $parentId === '0') {\n                            $query['filter']['has-parent'][$i] = false;\n                        }\n                    }\n                } elseif ($filterName === 'from') {\n                    try {\n                        $query['filter']['from'] = new DateTime($query['filter']['from']);\n                    } catch (Exception $e) {\n                        $query['filter']['from'] = false;\n                    }\n                } elseif ($filterName === 'frequency') {\n                    $query['filter'][$filterName] = self::normalizeInteger($filter);\n                }\n            }\n        }\n        // idem with contain clauses\n        if (isset($query['contain'])) {\n            if (!is_array($query['contain'])) {\n                throw new BadRequestException(__('Invalid query string. The contain parameter should be an array.'));\n            }\n            foreach ($query['contain'] as $containName => $contain) {\n                $query['contain'][$containName] = self::normalizeBoolean($contain);\n            }\n        }\n\n        return $query;\n    }\n\n    /**\n     * Extract array string items\n     *\n     * @param array $query original query string items\n     * @param array $allowedQueryItems whitelist\n     * @return array $query the sanitized query\n     */\n    public static function unsetUnwantedQueryItems(array $query, array $allowedQueryItems): array\n    {","sourceCodeStart":128,"sourceCodeEnd":164,"githubUrl":"https://github.com/passbolt/passbolt_api/blob/31c1bbc10f32808a607fa9bd81891e898779c0bc/src/Controller/Component/QueryStringComponent.php#L128-L164","documentation":"QueryStringComponent::normalizeQueryItems() requires the `contain` query parameter to be an array. If `contain` is present but not an array it throws BadRequestException('Invalid query string. The contain parameter should be an array.') producing a 400. Contain clauses tell the API which associations to embed, and each value is normalized to a boolean.","triggerScenarios":"GET collection/detail endpoints with `?contain=<scalar>` — e.g. `?contain=profile` instead of `?contain[]=profile`, or `?contain=1`; a contain value that URL parsing turns into a string rather than an array element.","commonSituations":"Clients omitting the `[]` in `contain[]=`; copying single-association examples written as `contain=groups` from other APIs; URL-encoding bugs dropping brackets; templated API wrappers joining contain values with commas into one string.","solutions":["Use array syntax: `?contain[]=profile` (or `?contain%5B%5D=profile`) instead of a bare scalar value.","For multiple associations repeat the parameter: `?contain[]=profile&contain[]=groups-users`.","Fix the client query builder so contain keys are emitted as arrays, and verify bracket URL-encoding.","If you don't need associations, omit the contain parameter entirely."],"exampleFix":"// before — scalar contain, rejected\nGET /users.json?contain=profile\n// after — array syntax\nGET /users.json?contain[]=profile\n// multiple associations:\nGET /users.json?contain[]=profile&contain[]=gpgkey","handlingStrategy":"type-guard","validationCode":"// ensure contain serializes as array query params\nfunction buildContainParams(contains) {\n  const p = new URLSearchParams();\n  for (const c of contains) p.append('contain[]', c); // emits contain[]=value\n  return p;\n}","typeGuard":"const isContainArray = (v) => v === undefined || v === null || Array.isArray(v); // a bare string like 'profile' is rejected server-side","tryCatchPattern":"try {\n  return await api.get('/users.json', { params });\n} catch (e) {\n  if (e.response?.status === 400 && /contain parameter should be an array/.test(e.response?.data?.message ?? '')) {\n    throw new Error('Send associations as contain[]=name, not contain=name');\n  }\n  throw e;\n}","preventionTips":["Always emit `contain[]=` (URL-encoded `contain%5B%5D=`) even for a single association.","Restrict contain names to associations documented for the endpoint to avoid follow-up 400s.","Share one query-serialization helper for filter/contain so both keep bracket array syntax.","Test query builders with encoded brackets to catch encoders that strip or escape `[]` incorrectly."],"tags":["query-string","contain","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"}