apereo/cas · error · IOException
The service definition file could not be saved at
Error message
The service definition file could not be saved at
What it means
The service registry computed a target file for the RegisteredService, but the underlying store (RepositoryFilter/RepositoryResult check inside the save lambda) returned false, so AbstractResourceBasedServiceRegistry.save aborts with an IOException stating the service definition file could not be written at that path. The registry's in-memory map is left unchanged.
Solutions
- Read the preceding debug-level log line (the caught exception's message) to see why the store returned false — it names the actual failure.
- Verify the service registry directory exists and the CAS process user can create/write files in it (chown/chmod, or fix cas.service-registry.json.directory).
- Check disk space and whether the mount is read-only (e.g. Kubernetes volume mounted ro).
- Review any configured custom RepositoryFilter beans for rules rejecting the file (duplicate serviceId, name patterns).
- If replication-related, ensure the shared filesystem is consistently mounted on all CAS nodes before retrying the save.
Example fix
// before: registry directory not writable by cas user cas.service-registry.json.directory=/etc/cas/services (owned by root, mode 755) // after: grant the CAS process write access chown cas:cas /etc/cas/services && chmod 750 /etc/cas/services
Defensive patterns
Strategy: try-catch
Validate before calling
File dir = new File(casServiceRegistryDirectory);
if (!dir.canWrite() || (dir.exists() && !dir.isDirectory()) && !dir.mkdirs()) {
// fix permissions/path before saving services
} Try / catch
try {
serviceRegistry.save(service);
} catch (IOException e) {
LOGGER.error("Service save failed: {}", e.getMessage());
// check registry dir permissions/space before retry
} Prevention
- Provision the registry directory with correct ownership for the CAS runtime user.
- Monitor disk space and mount flags (read-only) on registry volumes.
- Catch and surface the debug log emitted just before the IOException — it holds the root cause.
- Test service saves after deploying to new containers or volume layouts.
When it happens
Trigger: save() is called (add/save of a RegisteredService via the management console or registry API) and the storeIfNotPresent/overwrite lambda fails — typically because the service directory doesn't exist and can't be created, the process lacks write permission, a filter (e.g. an inline repository filter or duplicate serviceId check) rejects the file, or the filesystem is read-only/full.
Common situations: CAS service registry directory on a read-only mount or container volume; wrong cas.service-registry.* path permissions after running as a different user; disk full; a custom RepositoryFilter rejecting the save; running multiple CAS nodes with mismatched shared-filesystem setups.
Understand the failure class
Background: "failed to write file", "Could not save figure", "Error saving remote file" — file write failed: causes and fixes across languages and libraries — this error's family across 38 libraries.
Related errors
- Cannot read/load from subordinate directory
- GraalVM native image executable is unable to discover…
- [ ] is not readable. Check file permissions
- [ ] is not found at the path specified
- subordinate directory
AI-assisted analysis of apereo/cas@e7288fc434 (2026-09-08).
Data as JSON: /api/errors/e504dc515af31f83.
Report an issue: GitHub.
Appendix: source
Thrown at core/cas-server-core-services-registry/src/main/java/org/apereo/cas/services/resource/AbstractResourceBasedServiceRegistry.java:194
}
@Override
public @Nullable RegisteredService save(final RegisteredService service) {
service.assignIdIfNecessary();
val fileName = getRegisteredServiceFileName(service);
try (val out = Files.newOutputStream(fileName.toPath())) {
invokeServiceRegistryListenerPreSave(service);
val result = registeredServiceSerializers.stream().anyMatch(serializer -> {
try {
serializer.to(out, service);
return true;
} catch (final Exception e) {
LOGGER.debug(e.getMessage(), e);
return false;
}
});
if (!result) {
throw new IOException("The service definition file could not be saved at " + fileName.getCanonicalPath());
}
if (this.services.containsKey(service.getId())) {
LOGGER.debug("Found existing service definition by id [{}]. Saving...", service.getId());
}
services.put(service.getId(), service);
LOGGER.debug("Saved service to [{}]", fileName.getCanonicalPath());
} catch (final IOException e) {
throw new IllegalArgumentException("IO error opening file stream.", e);
}
return findServiceById(service.getId());
}
@Override
public boolean delete(final RegisteredService service) {
return lock.tryLock(() -> FunctionUtils.doUnchecked(() -> {
val filename = getRegisteredServiceFileName(service);
val clientInfo = ClientInfoHolder.getClientInfo();
publishEvent(new CasRegisteredServicePreDeleteEvent(this, service, clientInfo));View on GitHub (pinned to e7288fc434)