{"record":{"id":"c49bfb444c79e887","repo":"mybatis/mybatis-3","slug":"a-cursor-is-already-closed","errorCode":null,"errorMessage":"A Cursor is already closed.","messagePattern":"A Cursor is already closed\\.","errorType":"exception","errorClass":"IllegalStateException","httpStatus":null,"severity":"error","filePath":"src/main/java/org/apache/ibatis/cursor/defaults/DefaultCursor.java","lineNumber":100,"sourceCode":"  }\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();\n      }\n    } catch (SQLException e) {\n      // ignore","sourceCodeStart":82,"sourceCodeEnd":118,"githubUrl":"https://github.com/mybatis/mybatis-3/blob/008069adb1b089579b5dcba87ee591908b263274/src/main/java/org/apache/ibatis/cursor/defaults/DefaultCursor.java#L82-L118","documentation":"MyBatis DefaultCursor throws IllegalStateException when iterator() is called after the Cursor has been closed. Closing releases the underlying ResultSet (and on some configurations the Statement), so iteration is impossible afterwards. The closed flag is set by close(), by try-with-resources exit, or implicitly when the cursor is fully consumed and the session closes it.","triggerScenarios":"Calling iterator() outside a try-with-resources block after close() ran; returning a Cursor from a method that closes the SqlSession (or the try block) before the caller iterates; iterating a Cursor whose SqlSession was already closed/committed; using a Cursor after fully consuming it in a prior pass that also closed it.","commonSituations":"Returning Cursor<T> from a DAO where the SqlSession is closed by a template/interceptor before the caller consumes it; try-with-resources scoping mistake where the Cursor is created inside the resource block but consumed after it; mixing selectCursor with Spring's SqlSessionTemplate which closes the session on method return.","solutions":["Keep the SqlSession and Cursor open while consuming: create the Cursor and iterate inside the same scope (same try-with-resources or same open session)","If the consumer is in another layer, return List<T> instead of Cursor<T>, or materialize with a collector before closing","Check cursor.isOpen() before calling iterator() to fail gracefully","With Spring, use SqlSessionFactoryBean cursors within a @Transactional method or a SqlSessionTemplate.execute with CursorCallback handled in one block"],"exampleFix":"// before\npublic Cursor<User> users() {\n  try (SqlSession s = factory.openSession()) {\n    return s.selectCursor(\"findUsers\"); // session closes -> cursor closed\n  }\n}\n\n// after\npublic List<User> users() {\n  try (SqlSession s = factory.openSession();\n       Cursor<User> c = s.selectCursor(\"findUsers\")) {\n    List<User> out = new ArrayList<>();\n    c.forEach(out::add);\n    return out;\n  }\n}","handlingStrategy":"validation","validationCode":"if (cursor.isOpen()) {\n  Iterator<User> it = cursor.iterator(); // safe\n} else {\n  // cursor closed: fetch a new one via sqlSession.selectCursor(...)\n}","typeGuard":null,"tryCatchPattern":"try {\n  Iterator<User> it = cursor.iterator();\n} catch (IllegalStateException e) {\n  if (e.getMessage().contains(\"already closed\")) {\n    // reopen session/cursor or fall back to selectList\n  } else throw e;\n}","preventionTips":["Consume the Cursor inside the same try-with-resources block that opened the SqlSession","Do not return a Cursor from a scope that closes the session before consumption","Check isOpen() before iterating when ownership crosses method boundaries"],"tags":["mybatis","cursor","lifecycle","resource-leak","illegal-state"],"backgroundTag":null,"analyzedSha":"008069adb1b089579b5dcba87ee591908b263274","analyzedAt":"2026-08-14T13:07:10.264Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}