openzipkin/zipkin · error · IllegalArgumentException

No content reading cluster health

Error message

No content reading cluster health

What it means

ElasticsearchStorage.ensureIndexTemplatesAndClusterReady(index) performs the one-time index-template installation and then issues GET /_cluster/health/<index> through the READ_STATUS body converter. The converter is expected to always produce a CheckResult; result == null means the HTTP call completed but no content/body was converted — i.e. the cluster-health response was empty — and that is surfaced as IllegalArgumentException instead of a silent pass.

Source

Thrown at zipkin-storage/elasticsearch/src/main/java/zipkin2/elasticsearch/ElasticsearchStorage.java:296

  /** This is blocking so that we can determine if the cluster is healthy or not */
  @Override public CheckResult check() {
    return ensureIndexTemplatesAndClusterReady(indexNameFormatter().formatType(TYPE_SPAN));
  }

  /**
   * This allows the health check to display problems, such as access, installing the index
   * template. It also helps reduce traffic sent to nodes still initializing (when guarded on the
   * check result). Finally, this reads the cluster health of the index as it can go down after the
   * one-time initialization passes.
   */
  CheckResult ensureIndexTemplatesAndClusterReady(String index) {
    try {
      version(); // ensure the version is available (even if we already cached it)
      ensureIndexTemplates(); // called only once, so we have to double-check health
      AggregatedHttpRequest request = AggregatedHttpRequest.of(GET, "/_cluster/health/" + index);
      CheckResult result = http().newCall(request, READ_STATUS, "get-cluster-health").execute();
      if (result == null) throw new IllegalArgumentException("No content reading cluster health");
      return result;
    } catch (Throwable e) {
      Call.propagateIfFatal(e);
      // Wrapping interferes with humans intended to read this message:
      //
      // Unwrap the marker exception as the health check is not relevant for the throttle component.
      // Unwrap any IOException from the first call to ensureIndexTemplates()
      if (e instanceof RejectedExecutionException || e instanceof UncheckedIOException) {
        return CheckResult.failed(e.getCause());
      }
      return CheckResult.failed(e);
    }
  }

  volatile boolean ensuredTemplates;

  // synchronized since we don't want overlapping calls to apply the index templates
  void ensureIndexTemplates() {

View on GitHub (pinned to 878ce2a1fa)

Solutions

  1. curl -v the exact URL Zipkin uses (http://<host>:9200/_cluster/health/zipkin-span-<date>) and confirm a JSON body with a status field is returned.
  2. Fix the proxy/ingress in front of Elasticsearch so it passes the response body through unchanged.
  3. Verify ES_HOSTS points at real Elasticsearch/OpenSearch HTTP endpoints (port 9200, not the transport port).
  4. The exception is wrapped into CheckResult.failed by the surrounding catch, so the message appears in the health endpoint — use it to diagnose rather than crash.

Example fix

# before: proxy returns empty 200 for /_cluster/health/*
# -> IllegalArgumentException: No content reading cluster health

# after: verify direct node response and bypass/fix the proxy
$ curl -s http://es:9200/_cluster/health/zipkin-span-2026-08-14
{"cluster_name":"es","status":"green",...}
Defensive patterns

Strategy: retry

Validate before calling

// smoke-test the exact health URL before/at startup
try (var client = HttpClient.newHttpClient()) {
  var resp = client.send(HttpRequest.newBuilder(URI.create(esHost + "/_cluster/health/zipkin-span")).GET().build(),
      HttpResponse.BodyHandlers.ofString());
  if (resp.statusCode() != 200 || resp.body().isEmpty()) {
    throw new IllegalStateException("Proxy/node returned empty cluster health body");
  }
}

Try / catch

// health checks return CheckResult instead of throwing; inspect it
CheckResult health = storage.check();
if (!health.ok()) {
  // log health.error(); retry or alert — this error appears as its message
}

Prevention

When it happens

Trigger: The health-check path (CheckResult check() / first query after startup) when /_cluster/health/<zipkin*> returns an empty body: e.g. an intermediate proxy or load balancer answering 200 with zero bytes, a misrouted host, or a node closing the response early.

Common situations: Elasticsearch behind nginx/HAProxy/Kubernetes ingress that strips or buffers responses; connecting through a service mesh; a URL with a wrong port hitting a non-ES service.

Related errors


AI-assisted analysis of openzipkin/zipkin@878ce2a1fa (2026-08-14). Data as JSON: /api/errors/1450472e61826943. Report an issue: GitHub.