apolloconfig/apollo · error · ServiceException

Write app error.

Error message

Write app error. {}

What it means

Thrown by ConfigsExportService when serializing an App entity to JSON and writing it as a ZipEntry into the export ZipOutputStream fails with an IOException. The error wraps the underlying I/O failure (closed stream, disk full, broken pipe) and aborts the entire export operation. The '{}' placeholder is meant to receive the cause but is actually passed the exception object, not the app, due to argument ordering.

Solutions

  1. Check portal server disk space and ensure the temp/working directory for zip assembly has adequate free space.
  2. Verify the HTTP client is not timing out or disconnecting before the export completes; increase client read timeout for large app counts.
  3. Inspect the portal logs for the preceding 'Write error. {app}' line which logs the specific App and the underlying IOException cause.
  4. Ensure no code path calls zipOutputStream.close() or finish() before all forEach consumers complete.

Example fix

// before
} catch (IOException e) {
  logger.error("Write error. {}", app);
  throw new ServiceException("Write app error. {}", e);
}
// after - log app AND cause, pass both args
} catch (IOException e) {
  logger.error("Write error. app={}, cause={}", app, e.getMessage());
  throw new ServiceException("Write app error. app={}, cause={}", e, app, e.getMessage());
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Before export, verify the output stream is writable and disk has space
File tempDir = new File(System.getProperty("java.io.tmpdir"));
long freeSpace = tempDir.getUsableSpace();
if (freeSpace < MIN_EXPORT_DISK_BYTES) {
  throw new IllegalStateException("Insufficient disk space for export: " + freeSpace);
}

Try / catch

try {
  configsExportService.exportData(apps, envs, outputStream);
} catch (ServiceException e) {
  if (e.getMessage().contains("Write app error")) {
    log.error("Export failed during app write, likely I/O or disk issue", e);
    // notify user, retry with smaller scope
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling the config-export API endpoint that drives writeAppInfoToZip() while the ZipOutputStream is already closed, the servlet response OutputStream has been aborted by the client, or the disk/temp directory is full. The parallel forEach on apps combined with synchronized(zipOutputStream) means a single failed write propagates as ServiceException.

Common situations: Client cancels a large export download mid-stream (broken pipe); the portal runs in a container with a small /tmp or low disk quota; an older ZipOutputStream is reused after finish() was called; concurrent export requests sharing a stream.

Related errors


AI-assisted analysis of apolloconfig/apollo@d95fc18d11 (2026-08-14). Data as JSON: /api/errors/0d2a4c6c4881ec55. Report an issue: GitHub.

Appendix: source

Thrown at apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/ConfigsExportService.java:203

    // app admin permission filter
    return apps.stream().filter(isAppAdmin).collect(Collectors.toList());
  }

  private void writeAppInfoToZip(List<App> apps, ZipOutputStream zipOutputStream) {
    logger.info("to import app size = {}", apps.size());

    final Consumer<App> appConsumer = app -> {
      try {
        synchronized (zipOutputStream) {
          String fileName = ConfigFileUtils.genAppInfoPath(app);
          String content = gson.toJson(app);

          writeToZip(fileName, content, zipOutputStream);
        }
      } catch (IOException e) {
        logger.error("Write error. {}", app);
        throw new ServiceException("Write app error. {}", e);
      }
    };

    apps.forEach(appConsumer);
  }

  private void exportAppNamespaces(ZipOutputStream zipOutputStream) {
    List<AppNamespace> appNamespaces = appNamespaceService.findAll();

    logger.info("to import appnamespace size = {}", appNamespaces.size());

    Consumer<AppNamespace> appNamespaceConsumer = appNamespace -> {
      try {
        synchronized (zipOutputStream) {
          String fileName = ConfigFileUtils.genAppNamespaceInfoPath(appNamespace);
          String content = gson.toJson(appNamespace);

          writeToZip(fileName, content, zipOutputStream);

View on GitHub (pinned to d95fc18d11)