{"record":{"id":"1c64f051b354e2e4","repo":"mybatis/mybatis-3","slug":"cannot-open-more-than-one-iterator-on-a-cursor","errorCode":null,"errorMessage":"Cannot open more than one iterator on a Cursor","messagePattern":"Cannot open more than one iterator on a Cursor","errorType":"exception","errorClass":"IllegalStateException","httpStatus":null,"severity":"error","filePath":"src/main/java/org/apache/ibatis/cursor/defaults/DefaultCursor.java","lineNumber":97,"sourceCode":"  @Override\n  public boolean isOpen() {\n    return status == CursorStatus.OPEN;\n  }\n\n  @Override\n  public boolean isConsumed() {\n    return status == CursorStatus.CONSUMED;\n  }\n\n  @Override\n  public int getCurrentIndex() {\n    return rowBounds.getOffset() + cursorIterator.iteratorIndex;\n  }\n\n  @Override\n  public Iterator<T> iterator() {\n    if (iteratorRetrieved) {\n      throw new IllegalStateException(\"Cannot open more than one iterator on a Cursor\");\n    }\n    if (isClosed()) {\n      throw new IllegalStateException(\"A Cursor is already closed.\");\n    }\n    iteratorRetrieved = true;\n    return cursorIterator;\n  }\n\n  @Override\n  public void close() {\n    if (isClosed()) {\n      return;\n    }\n\n    ResultSet rs = rsw.getResultSet();\n    try {\n      if (rs != null) {\n        rs.close();","sourceCodeStart":79,"sourceCodeEnd":115,"githubUrl":"https://github.com/mybatis/mybatis-3/blob/008069adb1b089579b5dcba87ee591908b263274/src/main/java/org/apache/ibatis/cursor/defaults/DefaultCursor.java#L79-L115","documentation":"A MyBatis Cursor (streaming result iterator returned by SqlSession.selectCursor) allows only ONE Iterator to be opened during its lifetime. The Cursor class implements Iterable by returning its internal single iterator, and sets iteratorRetrieved=true on the first call. Calling iterator() again (directly, via for-each, or via forEach) throws IllegalStateException because two concurrent iterators would share and corrupt the same underlying ResultSet.","triggerScenarios":"Calling cursor.iterator() a second time; using the same Cursor object in two enhanced for-loops; calling cursor.forEach(x -> ...) after already iterating it with for-each; passing the Cursor to a method that iterates it after your code already did; re-streaming a partially consumed Cursor in a retry loop.","commonSituations":"Streaming large result sets with selectCursor and accidentally iterating twice (e.g., once for logging/counting, once for processing); helper utilities that accept Iterable and iterate internally; retry logic that re-consumes the same Cursor; Spring code that wraps the Cursor in a Stream and also loops over it.","solutions":["Fetch a new Cursor via a fresh sqlSession.selectCursor(...) call for each iteration pass","Materialize the results into a List once (e.g., new ArrayList<>(cursor) or cursor.stream().collect(toList())) if you need multiple passes","Track iteration yourself: only call iterator() once per Cursor instance and consume it fully (or close it) before discarding","If you need resumable streaming, use getCurrentIndex() to record position and re-query with a RowBounds offset from a new Cursor"],"exampleFix":"// before\ntry (Cursor<User> c = sqlSession.selectCursor(\"findUsers\")) {\n  c.forEach(u -> log.debug(u));           // iterator #1\n  for (User u : c) process(u);            // iterator #2 -> IllegalStateException\n}\n\n// after\ntry (Cursor<User> c = sqlSession.selectCursor(\"findUsers\")) {\n  List<User> users = new ArrayList<>();\n  c.forEach(users::add);                  // single iteration\n  users.forEach(u -> log.debug(u));\n  users.forEach(this::process);\n}","handlingStrategy":"validation","validationCode":"// DefaultCursor tracks retrieval internally but exposes no getter;\n// wrap it so iteration happens exactly once per Cursor.\nboolean iterated = false;\ntry (Cursor<User> c = sqlSession.selectCursor(\"findUsers\")) {\n  if (iterated) throw new IllegalStateException(\"cursor already iterated\");\n  iterated = true;\n  c.forEach(this::process);\n}","typeGuard":null,"tryCatchPattern":"// Only if a stray second iteration is possible:\ntry {\n  for (User u : cursor) { ... }\n} catch (IllegalStateException e) {\n  if (e.getMessage().contains(\"more than one iterator\")) {\n    // re-open: fetch a fresh Cursor from a new selectCursor call\n  } else throw e;\n}","preventionTips":["Treat a Cursor as single-shot: one iterator per instance, then close","Materialize to a List when you need multiple passes or to hand data across layers","Never pass a Cursor to utility code that iterates Iterables without tracking consumption"],"tags":["mybatis","cursor","streaming","iterator","illegal-state"],"backgroundTag":null,"analyzedSha":"008069adb1b089579b5dcba87ee591908b263274","analyzedAt":"2026-08-14T13:07:10.264Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}