openzipkin/zipkin · error · IllegalArgumentException

Expected start object, was %s

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.

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)

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.