getgrav/grav · error · InvalidArgumentException
400
400
Error message
%s: Relationship %s cannot be modified
What it means
During save, UserObject::updateRelationships() (line 745) iterates modified relationships and only knows how to persist 'avatar' (via updateAvatarRelationship). Any other modified relationship hits the default branch and throws InvalidArgumentException with HTTP code 400. Relationships like 'media' are readable but read-only through this API, so the error means your payload tried to write a relation the type does not support.
Source
Thrown at system/src/Grav/Common/Flex/Types/Users/UserObject.php:745
throw new \InvalidArgumentException(sprintf('%s: Relationship %s does not exist', $this->getFlexType(), $name));
}
/**
* @return bool Return true if relationships were updated.
*/
protected function updateRelationships(): bool
{
$modified = $this->getRelationships()->getModified();
if ($modified) {
foreach ($modified as $relationship) {
$name = $relationship->getName();
switch ($name) {
case 'avatar':
\assert($relationship instanceof ToOneRelationshipInterface);
$this->updateAvatarRelationship($relationship);
break;
default:
throw new \InvalidArgumentException(sprintf('%s: Relationship %s cannot be modified', $this->getFlexType(), $name), 400);
}
}
$this->resetRelationships();
return true;
}
return false;
}
/**
* @param ToOneRelationshipInterface $relationship
*/
protected function updateAvatarRelationship(ToOneRelationshipInterface $relationship): void
{
$files = [];
$avatar = $this->getAvatarImage();View on GitHub (pinned to 6040efed04)
Solutions
- Drop the non-avatar relationship change from the modification set before save() — user media is managed through media uploads/fields, not the relationship API.
- Validate submitted relationship names against ['avatar'] and reject others with your own 400/422 before save() is reached.
- Catch InvalidArgumentException and map it to an HTTP 400 naming the read-only relationship so clients get an actionable message.
Example fix
// before
foreach ($payload['relationships'] ?? [] as $name => $data) {
$user->getRelationships()->get($name)->update($data);
}
$user->save(); // 400: Relationship media cannot be modified
// after
foreach ($payload['relationships'] ?? [] as $name => $data) {
if ('avatar' !== $name) {
continue; // or reject with 422
}
$user->getRelationships()->get('avatar')->update($data);
}
$user->save(); Defensive patterns
Strategy: validation
Validate before calling
foreach ($payload['relationships'] ?? [] as $name => $_) {
if ('avatar' !== $name) {
unset($payload['relationships'][$name]); // or reject with 422
}
} Type guard
function isWritableUserRelationship(string $name): bool
{
return 'avatar' === $name;
} Try / catch
try {
$user->save();
} catch (\InvalidArgumentException $e) {
if (str_contains($e->getMessage(), 'cannot be modified')) {
// return 400 listing the read-only relationship
}
throw $e;
} Prevention
- Document which relationships are read-only ('media') vs writable ('avatar') in your API layer.
- Strip unknown relationship keys from payloads before mutation, not after.
- Manage user media via upload/field APIs, never via the relationship writer.
When it happens
Trigger: Saving a user while a relationship other than avatar is marked modified in the relationship collection — e.g. mutating the 'media' relationship object before save(), or a PATCH/POST to an endpoint that maps client-submitted relationship names onto the object; programmatic attempts to attach media files as relationships.
Common situations: REST integrations that try to modify user media through the generic flex relationship endpoint; plugins ported from flex types that do support writable relationships; stale client payloads carrying relationship changes the server cannot apply.
Related errors
- %s: Relationship %s does not exist
- Passwords did not match.
- Invalid argument $value
- Invalid argument $element
- __METHOD__(): You need to pass option 'directory'
AI-assisted analysis of getgrav/grav@6040efed04 (2026-08-17).
Data as JSON: /api/errors/fda1f906977a563b.
Report an issue: GitHub.