openzipkin/zipkin · error · IllegalArgumentException

Expected start object, was

Error message

Expected start object, was %s

What it means

JsonReaders.checkStartObject(parser, shouldThrow) verifies the parser is positioned on (or can advance to) a START_OBJECT token. With shouldThrow=true, any other token — or a non-JSON body, which makes the parser throw — is surfaced as IllegalArgumentException('Expected start object, was <token>'). It is used when entering objects of search responses, so it fires when the response is not the JSON object shape the client expects.

Solutions

  1. Capture the raw HTTP response the storage receives (curl the same URL Zipkin uses) and compare it with a known-good Elasticsearch/OpenSearch response.
  2. Remove the intermediary rewriting responses, or point ES_HOSTS directly at real ES nodes.
  3. Match the zipkin-server version to your cluster version so expected response shapes agree.
  4. Check ES credentials/permissions if the body is an auth error delivered with 200.

Example fix

# before: ES_HOSTS=http://es:9300  (transport port, returns non-JSON)
# -> IllegalArgumentException: Expected start object, was ...

# after: ES_HOSTS=http://es:9200  (HTTP port returning JSON objects)
Defensive patterns

Strategy: try-catch

Validate before calling

// confirm endpoints return JSON objects before wiring storage
var resp = http.get(esHost + "/_cluster/health");
if (!resp.body().trim().startsWith("{")) {
  throw new IllegalStateException("Expected JSON object from ES, got: " + resp.body().substring(0, 80));
}

Try / catch

try {
  V result = call.execute();
} catch (IllegalArgumentException e) {
  if (e.getMessage().startsWith("Expected start object")) {
    // response is not the JSON object shape expected: check proxy/auth/ES version skew
  }
  throw e;
}

Prevention

When it happens

Trigger: An Elasticsearch search/cluster call returns a JSON array, a scalar, an error envelope at the wrong depth, or a non-JSON body (HTML error page, empty-but-status-200) at a point where Zipkin's internal readers call checkStartObject with shouldThrow=true.

Common situations: A proxy or captive portal returning HTML; ES returning an error body with HTTP 200; version-skew where response shapes changed; hitting a non-ES port.

Related errors


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

Appendix: source

Thrown at zipkin-storage/elasticsearch/src/main/java/zipkin2/elasticsearch/internal/JsonReaders.java:105

        break;
      case START_OBJECT:
        visitObject(parser, name, result);
        break;
      default:
        // Skip current value.
    }
  }

  static boolean checkStartObject(JsonParser parser, boolean shouldThrow) throws IOException {
    try {
      JsonToken currentToken = parser.currentToken();
      // The parser may not be at a token, yet. If that's the case advance.
      if (currentToken == null) currentToken = parser.nextToken();

      // If we are still not at the expected token, we could be an another or an empty body.
      if (currentToken == JsonToken.START_OBJECT) return true;
      if (shouldThrow) {
        throw new IllegalArgumentException("Expected start object, was " + currentToken);
      }
      return false;
    } catch (Throwable e) { // likely not json
      if (shouldThrow) throw e;
      return false;
    }
  }

  JsonReaders() {
  }
}

View on GitHub (pinned to 878ce2a1fa)