passbolt/passbolt_api · error · Cake\Http\Exception\BadRequestException

The request is already completed.

Error message

The request is already completed.

What it means

BadRequestException from getNotCompletedOrFail when the matched account recovery request is already in a completed status (approved, rejected or completed). Completed requests cannot be fetched/processed again through this path.

Solutions

  1. Start a new account recovery request (new token) instead of reusing the completed one
  2. Check the request's `status` before calling; only 'pending' requests are fetchable via this method
  3. If recovery was rejected/completed, follow the returned outcome (e.g. complete setup or normal login)

Example fix

// before
getNotCompletedOrFail($requestId, ...) // status = 'approved' -> 400
// after
create a fresh request via AccountRecoveryRequestCreateService, then get the new request id
Defensive patterns

Strategy: validation

Validate before calling

$request = $this->AccountRecoveryRequests->get($requestId); if ($request->isCompleted()) { // skip; start a new recovery flow instead }

Type guard

null

Try / catch

try { $request = $service->get($requestId, $userId, $token); } catch (BadRequestException $e) { if (str_contains($e->getMessage(), 'already completed')) { startNewRecoveryFlow(); } }

Prevention

When it happens

Trigger: GET /account-recovery/requests/{id} for a request whose status has already transitioned out of 'pending' — e.g. an admin already responded, or a previous recovery attempt finalized it.

Common situations: Client double-submits or refreshes after the request was completed; admin approved/rejected while the user's client retries; user retries recovery after already completing it once.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


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

Appendix: source

Thrown at plugins/PassboltEe/AccountRecovery/src/Service/AccountRecoveryRequests/AccountRecoveryRequestGetService.php:174

        if (!Validation::uuid($requestId)) {
            throw new BadRequestException(__('The request id is invalid.'));
        }
        try {
            $where = [
                'id' => $requestId,
                'user_id' => $userEntity->id,
                'authentication_token_id' => $tokenEntity->id,
            ];
            /** @var \Passbolt\AccountRecovery\Model\Entity\AccountRecoveryRequest $requestEntity */
            $requestEntity = $this->AccountRecoveryRequests->find()->where($where)->firstOrFail();
        } catch (RecordNotFoundException $exception) {
            $this->onRequestDoesNotExist($requestId, $userId, $clientIp ?? '0.0.0.0');
            throw new NotFoundException(__('The account recovery request could not be found.'));
        }

        // Assert request is not already completed
        if ($requestEntity->isCompleted()) {
            throw new BadRequestException(__('The request is already completed.'));
        }
        // Assert token is not expired. If so, deactivate the token, reject the request and throw an exception
        if ($tokenEntity->isExpired()) {
            $requestEntity->set('status', AccountRecoveryRequest::ACCOUNT_RECOVERY_REQUEST_REJECTED);
            $requestEntity->setAccess('status', true);
            $this->AccountRecoveryRequests->saveOrFail($requestEntity);
            $tokenService->getActiveNotExpiredOrFail($token, $userId, AuthenticationToken::TYPE_RECOVER);
        }

        return $requestEntity;
    }

    /**
     * @param \Passbolt\AccountRecovery\Model\Entity\AccountRecoveryRequest $requestEntity entity
     * @return array
     */
    public function decorateResults(AccountRecoveryRequest $requestEntity): array
    {

View on GitHub (pinned to 31c1bbc10f)