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

  1. Read the preceding debug-level log line (the caught exception's message) to see why the store returned false — it names the actual failure.
  2. 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).
  3. Check disk space and whether the mount is read-only (e.g. Kubernetes volume mounted ro).
  4. Review any configured custom RepositoryFilter beans for rules rejecting the file (duplicate serviceId, name patterns).
  5. 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

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


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)