passbolt/passbolt_api · error · BadRequestException

Invalid query string. The filter parameter should be an…

Error message

Invalid query string. The filter parameter should be an array.

What it means

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.

Solutions

  1. Send filters using array syntax: `?filter[]=has-managers` or named filters `?filter[is-deleted]=true`.
  2. To request no filters, either omit the parameter entirely or send the literal `?filter=[]` (explicitly normalized to an empty array).
  3. Check URL encoding — `[]` must survive encoding (`filter%5B%5D=...`); fix client-side query builders that encode brackets incorrectly.
  4. If using a JSON body style, stop: this API expects bracket-array query strings, not JSON in query parameters.

Example fix

// before — scalar filter, rejected
GET /users.json?filter=has-access
// after — array syntax
GET /users.json?filter[]=has-access
// or multiple:
GET /users.json?filter[]=is-admin&filter[]=active
Defensive patterns

Strategy: type-guard

Validate before calling

// ensure filters serialize as array query params
function buildFilterParams(filters) {
  const p = new URLSearchParams();
  for (const f of filters) p.append('filter[]', f); // emits filter[]=value
  return p;
}

Type guard

const isFilterArray = (v) => v === undefined || v === null || Array.isArray(v) || (typeof v === 'object' && v !== null); // never a bare string/number

Try / catch

try {
  return await api.get('/users.json', { params });
} catch (e) {
  if (e.response?.status === 400 && /filter parameter should be an array/.test(e.response?.data?.message ?? '')) {
    throw new Error('Send filters as filter[]=value, not filter=value');
  }
  throw e;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Background: "Invalid query parameter" / "Failed to parse value of ...": fixing bad query string parameters across APIs — this error's family across 36 libraries.

Related errors


AI-assisted analysis of passbolt/passbolt_api@31c1bbc10f (2026-09-17). Data as JSON: /api/errors/83a2d48289a26469. Report an issue: GitHub.

Appendix: source

Thrown at src/Controller/Component/QueryStringComponent.php:113

     * @param array $query original query string items
     * @return array modified query
     */
    public static function normalizeQueryItems(array $query): array
    {
        // order should always be an array even when one value is provided
        // this is deprecated, order is now handled by the ApiPaginationComponent
        if (isset($query['order']) && !is_array($query['order'])) {
            $query['order'] = [$query['order']];
        }

        // filters with is-* means we are expecting a boolean
        // we accept 'TRUE', 'true', '1' as true and the rest is set to false
        if (isset($query['filter'])) {
            if ($query['filter'] == '[]') {
                $query['filter'] = [];
            }
            if (!is_array($query['filter'])) {
                throw new BadRequestException(__('Invalid query string. The filter parameter should be an array.'));
            }
            foreach ($query['filter'] as $filterName => $filter) {
                if (!is_string($filterName)) {
                    continue;
                }
                if (in_array($filterName, self::mustBeArrayFilters())) {
                    // these should always be an array
                    $query['filter'][$filterName] = $filter = (array)$query['filter'][$filterName];
                }
                $booleanFilters = ['deleted', 'expired'];
                if (substr($filterName, 0, 3) === 'is-' || in_array($filterName, $booleanFilters)) {
                    $query['filter'][$filterName] = self::normalizeBoolean($filter);
                } elseif ($filterName === 'has-parent') {
                    foreach ($query['filter']['has-parent'] as $i => $parentId) {
                        if ($parentId === 'false' || $parentId === '0') {
                            $query['filter']['has-parent'][$i] = false;
                        }
                    }

View on GitHub (pinned to 31c1bbc10f)