passbolt/passbolt_api · error · BadRequestException

Account recovery case must be a string.

Error message

Account recovery case must be a string.

What it means

Thrown by UserRecoverService::assertRecoveryCase when the 'case' field in the request body is present but is not a JSON string (e.g. a number, boolean, array or object). The recovery endpoint accepts only the whitelisted string cases (default, lost-token, etc.); a non-string payload is rejected as a bad request before the enum check.

Solutions

  1. Send 'case' as a quoted string in the JSON body: "case": "lost-token".
  2. Use only supported values from ACCOUNT_RECOVERY_CASES (see UserRecoverService) — anything else triggers the related 'reason not supported' error.
  3. Omit 'case' entirely to get the default case if you do not need a specific one.
  4. Check your client code/curl payload for accidental numeric or nested values.

Example fix

// before
curl -d '{"username":"u@x.com","case":3}' /recover.json

// after
curl -d '{"username":"u@x.com","case":"lost-token"}' /recover.json
Defensive patterns

Strategy: type-guard

Validate before calling

function assertCase(v) {
  if (v === undefined || v === null) return 'default';
  if (typeof v !== 'string') throw new TypeError('case must be a string');
  return v;
}
body.case = assertCase(rawCase);

Type guard

const isString = (v) => typeof v === 'string';
const safeCase = isString(input.case) ? input.case : undefined;

Try / catch

try {
  await recover({ username, case: safeCase });
} catch (e) {
  if (e.response?.status === 400 && e.response.body?.message?.includes('must be a string')) {
    console.error('Send "case" as a quoted JSON string, e.g. "lost-token"');
  }
}

Prevention

When it happens

Trigger: POST /recover.json with body like {"username": "a@b.com", "case": 123} or {"case": ["lost-token"]} — any non-string 'case' value.

Common situations: Client SDKs or scripts building the JSON body with the wrong type; YAML/JSON config being parsed where a quoted value loses its string type (e.g. case: 1 instead of case: '1'); copy-pasted curl examples missing quotes.

Understand the failure class

Background: Type mismatch errors: IllegalArgumentException, TypeError and type guards across 150 open-source libraries — this error's family across 150 libraries.

Related errors


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

Appendix: source

Thrown at src/Service/Users/UserRecoverService.php:137

    {
        $viewBuilder
            ->setTemplatePath('Auth')
            ->setLayout('default')
            ->setTemplate('triage');
    }

    /**
     * @return string self::ACCOUNT_RECOVERY_CASE_DEFAULT default
     */
    protected function assertRecoveryCase(): string
    {
        $case = $this->request->getData('case') ?? null;

        if (!isset($case)) {
            return self::ACCOUNT_RECOVERY_CASE_DEFAULT;
        }
        if (!is_string($case)) {
            throw new BadRequestException(__('Account recovery case must be a string.'));
        }
        if (!in_array($case, self::ACCOUNT_RECOVERY_CASES)) {
            throw new BadRequestException(__('Account recovery reason not supported.'));
        }

        return $case;
    }

    /**
     * Assert some username data is provided
     *
     * @throws \Cake\Http\Exception\BadRequestException if the username is not valid
     * @throws \Cake\Http\Exception\BadRequestException if the username is not provided
     * @return string validated username
     */
    protected function assertUsername(): string
    {
        $username = $this->request->getData('username') ?? null;

View on GitHub (pinned to 31c1bbc10f)