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
- Grant the acting user save access on the flex type via user/group access rules or the directory's authorization configuration.
- Verify authentication before calling check(): if the request is anonymous, respond 401/login instead of invoking the gate.
- 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
- Authenticate first: anonymous requests should never reach check().
- Pre-check isAuthorized('save', ...) when you need control flow instead of exceptions.
- Test ACL rules for each flex type after changing access configuration.
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
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- Bad child
- json_encode(): failed to encode dependencies
- json_encode(): failed to encode group permissions
- Unable to %s: %s
- Unable to create directory: %s
AI-assisted analysis of getgrav/grav@6040efed04 (2026-08-17).
Data as JSON: /api/errors/cd8b9b1fb425aede.
Report an issue: GitHub.