getgrav/grav · error · RuntimeException

403

403

Error message

Forbidden

What it means

FlexObject::check(?UserInterface $user) (line 738) is the pre-save authorization gate: when a user is supplied and isAuthorized('save', null, $user) returns false, it throws RuntimeException('Forbidden', 403). Authorization comes from the flex type's configured ACL rules combined with the acting user's access, so this error is about permissions, not about the object's data.

Source

Thrown at system/src/Grav/Framework/Flex/FlexObject.php:738

    /**
     * @param string|null $key
     * @return FlexObject|FlexObjectInterface
     */
    public function createCopy(?string $key = null)
    {
        $this->markAsCopy();

        return $this->create($key);
    }

    /**
     * @param UserInterface|null $user
     */
    public function check(?UserInterface $user = null): void
    {
        // If user has been provided, check if the user has permissions to save this object.
        if ($user && !$this->isAuthorized('save', null, $user)) {
            throw new \RuntimeException('Forbidden', 403);
        }
    }

    /**
     * {@inheritdoc}
     * @see FlexObjectInterface::save()
     */
    public function save()
    {
        $this->triggerEvent('onBeforeSave');

        $storage = $this->getFlexDirectory()->getStorage();

        $storageKey = $this->getStorageKey() ?:  '@@' . spl_object_hash($this);

        $result = $storage->replaceRows([$storageKey => $this->prepareStorage()]);

        if (method_exists($this, 'clearMediaCache')) {

View on GitHub (pinned to 6040efed04)

Solutions

  1. Grant the acting user save access on the flex type via user/group access rules or the directory's authorization configuration.
  2. Verify authentication before calling check(): if the request is anonymous, respond 401/login instead of invoking the gate.
  3. Use the non-throwing twin $object->isAuthorized('save', null, $user) for control flow, or catch RuntimeException with code 403 and surface an HTTP 403 naming the flex type.

Example fix

// before
$object->check($user); // throws RuntimeException('Forbidden', 403)
$object->save();

// after
if (!$object->isAuthorized('save', null, $user)) {
    return $response->withStatus(403);
}
$object->save();
Defensive patterns

Strategy: validation

Validate before calling

if (null === $user || $user->authenticated === false) {
    // return 401/login instead of invoking check()
}
if (!$object->isAuthorized('save', null, $user)) {
    // return 403 before calling check()/save()
}

Type guard

function canSaveFlexObject(\Grav\Framework\Flex\FlexObject $object, ?\Grav\Common\User\Interfaces\UserInterface $user): bool
{
    return null === $user || $object->isAuthorized('save', null, $user);
}

Try / catch

try {
    $object->check($user);
} catch (\RuntimeException $e) {
    if (403 === $e->getCode()) {
        // map to HTTP 403, include the flex type in the error payload
    }
    throw $e;
}

Prevention

When it happens

Trigger: Calling $object->check($currentUser) before save/delete when the user lacks save permission on that flex type; flex REST/API endpoints invoked by unauthenticated clients or users whose access rules deny the type; admin controllers delegating validation to check().

Common situations: API requests with missing/expired credentials so the user resolves to anonymous; new ACL rules for a flex type that forget to grant save to the editing role; front-end forms letting non-privileged users submit flex objects.

Understand the failure class

Related errors


AI-assisted analysis of getgrav/grav@6040efed04 (2026-08-17). Data as JSON: /api/errors/cd8b9b1fb425aede. Report an issue: GitHub.